Skip to content

fix(bind): keep the numeric setter adapter transparent - #245

Closed
linkdata wants to merge 1 commit into
mainfrom
fix/bind-float64-setter-transparency
Closed

fix(bind): keep the numeric setter adapter transparent#245
linkdata wants to merge 1 commit into
mainfrom
fix/bind-float64-setter-transparency

Conversation

@linkdata

@linkdata linkdata commented Aug 7, 2026

Copy link
Copy Markdown
Owner

Two defects in bind.MakeSetterFloat64's adapter, with one root cause: the adapter is not transparent to jaws.Element.ApplyGetter.

1. Binder hooks silently dropped (the user-visible one)

setterFloat64[T] embeds Setter[T], which promotes only JawsGet and JawsSet. ApplyGetter therefore never sees the wrapped Binder's JawsClick, JawsContextMenu or JawsInitialHTMLAttr, and every integer, float32 and uintptr binding loses them without a diagnostic:

rw.Number(bind.New(&mu, &n).InitialHTMLAttr(fn))  // fn never called
rw.Number(bind.New(&mu, &n).Clicked(fn))          // fn never called

Fixed by forwarding the four optional interfaces explicitly. Each reports the event as unhandled when the wrapped Setter does not implement it, which is what ApplyGetter and the event dispatch already expect for a plain setter, so dispatch still falls through to the widget.

2. float64 writes were never sanitized

MakeSetterFloat64 returned a Setter[float64] unwrapped, so it was the one settable type that never reached sanitizeFloatForT. float32 and the integer types reject NaN/Inf; float64 stored them:

bind.MakeSetterFloat64(bind.New(&mu, &f32)).JawsSet(nil, math.NaN())  // ErrFloatNotFinite
bind.MakeSetterFloat64(bind.New(&mu, &f64)).JawsSet(nil, math.NaN())  // nil — f64 is now NaN

A stored NaN permanently defeats the value != *b.ptr comparison binder.JawsSetLocked uses to detect change, so every later set reports "changed" and re-dirties — precisely the failure the guard exists to prevent. For a value bound to ui.Number/ui.Range it then cancels the Request on the next render or update.

float64 escaped the guard for the same reason it escaped #1: it was passed through instead of wrapped. Wrapping it like every other numeric type costs no behavior now that the adapter is transparent — sanitizeFloatForT[float64] takes the finiteness-only default branch, reports mayAlias false, and T(value) is the identity, so no conversion or extra JawsGet is introduced.

MakeSetterFloat64's doc now states the rejection contract, since callers can see ErrFloatNotFinite/ErrFloatOutOfRange from any binding.

lib/ui widgets were not affected by #2 in practice: InputFloat re-checks finiteness on all three entry points before calling the setter. Anyone building directly on bind had no such guard.

Scope note

setterFloat64ReadOnly[T] has the same opaque-wrapper shape for Getter values. It is left alone here: a Binder always matches the Setter case first, so nothing regresses through it, but it is the same latent gap.

Tests

  • lib/bind: non-finite parity across float64/float32/int; float64 pass-through behavior including ErrValueUnchanged and tag identity through TagExpand; interface forwarding in all three states (delegating, input-handler, plain setter).
  • lib/ui: end-to-end guard that a Binder's Clicked/InitialHTMLAttr and its pointer-derived dirty tag survive the adapter, for both the float64 and the converting int path.

Each new test was confirmed to fail on main for the stated reason before the fix.

Verification

go generate, go vet, gofmt -l ., staticcheck, golangci-lint run (0 issues), go test -race ./..., go test -tags debug -race ./..., and the production (non-race) leg all pass. Statement coverage stays at 100% in both lib/bind and lib/ui. The 386 leg compiles and vets locally; it cannot execute in this environment, so CI covers it.

setterFloat64 embeds Setter[T], which promotes only JawsGet and JawsSet, so
Element.ApplyGetter never saw a wrapped Binder's JawsClick, JawsContextMenu or
JawsInitialHTMLAttr. Every integer, float32 and uintptr binding silently
dropped those hooks:

	rw.Number(bind.New(&mu, &n).InitialHTMLAttr(fn))  // fn never called

Forward the four optional interfaces explicitly, each reporting the event as
unhandled when the wrapped Setter does not implement it, which is what
ApplyGetter and the event dispatch already expect for a plain setter.

MakeSetterFloat64 also returned a Setter[float64] unwrapped, making it the one
settable type that never reached sanitizeFloatForT: float32 and the integer
types rejected NaN and Inf while float64 stored them. A stored NaN permanently
defeats the equality comparison binder.JawsSetLocked uses to detect change, and
makes ui.Number and ui.Range cancel the Request on the next render. Wrap it
like every other numeric type, which costs no behavior now that the adapter is
transparent.
@linkdata

linkdata commented Aug 7, 2026

Copy link
Copy Markdown
Owner Author

Closing as invalid in favor of #254. Wrapping an existing Setter[float64] changes its dynamic interface set, breaks Binder HTMLGetter/Format composition, and makes plain setters appear to be event handlers. InputFloat already enforces finite values at the UI boundary. The remaining converted-Binder behavior issue is tracked in #254.

@linkdata linkdata closed this Aug 7, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant