← board

Callable options for the tkinter façade — LANDED 2026-07-28

Done: Tcl_CreateCommand in lib/pcl/tk.pas, one pxxcb dispatcher, a callback registry and an Event object in tkinter.pas, and three callable shapes reaching Tk — a BOUND METHOD (self._on_wheel), a plain def name, and a lambda. bind(..., add="+"), Checkbutton(command=...) and BooleanVar.trace_add all route through it. Example: examples/tk/callbacks.npy (compiled by the suite, run under Xvfb).

What it needed on the compiler side: pycallback_call0/1 in pylib (call a function value from library code), a bare def NAME as a value (PyMakeFuncValue), keyword arguments on overloaded and field-receiver method calls, and a real super().__init__.

REMAINING for songformatter: a lambda that calls a METHOD on a captured object — [[feature-nilpy-lambda-compiled-closure]].

What blocks

widget.bind("<MouseWheel>", self._on_mousewheel, add="+")
button = tk.Button(parent, text="OK", command=self.save)

The façade takes a Tcl SCRIPT STRING for both. Real applications pass a callable, and it is usually a BOUND METHOD, so the callback carries a receiver.

Shape

lib/pcl/tk.pas today only Tcl_Evals strings — no command registration. Add:

  1. Tcl_CreateCommand (an external 'libtcl8.6.so.0' next to Tcl_Eval) and ONE dispatcher registered as pxxcb, whose client data is an index into a table of registered callables.
  2. A registry in tkinter.pas: function TkiRegisterCallback(cb): Integer, returning the index; the Tcl side then gets {pxxcb 7 %d %x %y}.
  3. The callable itself. NilPy function values already have ONE ABI ([[bug-nilpy-callable-return-abi-mismatch]]) — variant parameters, variant result — which is exactly what a generic dispatcher needs. A BOUND method additionally needs its receiver; PyMakeBoundMethod already exists for the variant path, so check whether its value survives into a Callable[...] slot.
  4. The event object handlers read: .delta, .num, .width, .height, .x, .y, .widget. Tk substitutes those as %-codes in the script, so the dispatcher can fill a small event record from its arguments.
  5. add="+" on bind (append rather than replace) — one extra parameter.

Why it matters

It is the single biggest remaining item for the GUI MVP: settings.py, convertrawtext.py's editor and SongFormatter.py all stop here. Part of [[feature-nilpy-tkinter-facade-widening]], filed separately because it is a mechanism, not more surface.

Gate

make test-nilpy (compile-only for the façade, as the other tkinter tests are), an examples/tk/ demo that runs under Xvfb and prints what the callback saw, and settings.py getting past line 144.

VERIFIED and CLOSED 2026-07-31

examples/tk/callbacks.npy was compile-only in test-nilpy, and RUNNING it showed the feature was not actually whole: it died with 'Scrollbar' object has no attribute 'set' — the scrollbar wiring (yscrollcommand=self.bar.set) takes a bound method as a VALUE, and the frontend's trailing-underscore fallback only fires when a call follows. Fixed by naming the method what CPython names it ([[bug-lib-tkinter-trailing-underscore-params-block-kwargs]]).

The example now runs clean under Xvfb — bound method, plain def, lifted lambda, two variable traces, a canvas tag query — and its full output is ASSERTED in lib-test's tk-nilpy step rather than merely compiled, so the next regression of this kind is caught by Track B's own gate.

The remaining item named above, a lambda calling a method on a captured object, is [[feature-nilpy-lambda-compiled-closure]] (Track N) and works today via the lift path — callbacks.npy exercises it.

Log