Skip to content

compound: optional step on a numeric binding - #28

Merged
defiantnerd merged 1 commit into
mainfrom
compound-bind-step
Aug 24, 2026
Merged

defiantnerd merged 1 commit into
mainfrom
compound-bind-step

Conversation

@defiantnerd

Copy link
Copy Markdown
Owner

A binding maps an attribute to a property linearly - scale * x + offset - which
is right for a continuous control and wrong for a segmented one. A meter drawn as
discrete LED segments, a knob that should land on detents, a stepped bar: all of
them need the display quantised while the attribute stays continuous, and that
was not expressible. The alternative was quantising at the source, which changes
the value everything else reads.

step on CompoundBinding snaps the attribute value to the nearest multiple
before the linear map, so one step is the same fraction of the range whatever the
property it drives is measured in. 0 (the default) is smooth, so every existing
binding is untouched.

No ABI change

There is no bind-with-step entry point in the vtable and this needs none: the step
travels through the existing float setter as "<prop>.step" (e.g. "width.step"),
the same route stroke_dasharray takes to travel as a string. It is also kept in a
per-layer bind_steps map, so setting it before the bind - or re-binding the prop
afterwards - does not lose it, and setting it on an unbound prop cannot conjure a
phantom binding that would read attribute "" as 0.

The loader parses "step" inside a bind object and the serializer emits it when
non-zero, so a component document round-trips.

Files

  • hosts/shared/compound.h - the field, the quantisation in eval_binding_float,
    the "<prop>.step" setter, and apply_bind preserving it.
  • hosts/shared/component_loader.h - parse + emit.
  • include/neui/d/compound.h - the prop and the bind semantics documented.
  • tests/ - the quantisation itself (rounding at the halfway point, reaching the
    full range, step 0 leaving the value alone), both setter orders plus the
    re-bind, and the document round-trip.

Testing

neui_tests: 317 cases / 1863 checks pass, 4 of them new.

A binding maps an attribute to a property linearly - scale * x + offset - which
is right for a continuous control and wrong for a segmented one. A meter drawn
as discrete LED segments, a knob that should land on detents, a stepped bar: all
of them need the *display* quantised while the attribute stays continuous, and
that was not expressible. The alternative was quantising at the source, which
changes the value everything else reads.

`step` on CompoundBinding snaps the attribute value to the nearest multiple
before the linear map, so one step is the same fraction of the range whatever
the property it drives is measured in. 0 (the default) is smooth, so every
existing binding is untouched.

There is no bind-with-step entry point in the vtable and this needs none: the
step travels through the existing float setter as "<prop>.step" (e.g.
"width.step"), the same route stroke_dasharray takes to travel as a string. It
is also kept in a per-layer bind_steps map, so setting it before the bind - or
re-binding the prop afterwards - does not lose it, and setting it on an unbound
prop cannot conjure a phantom binding that would read attribute "" as 0.

The loader parses "step" inside a bind object and the serializer emits it when
non-zero, so a component document round-trips.

Tests cover the quantisation itself (rounding at the halfway point, reaching the
full range, step 0 leaving the value alone), both setter orders plus the re-bind,
and the document round-trip.
@defiantnerd
defiantnerd merged commit 58e7cd9 into main Aug 24, 2026
8 checks passed
@defiantnerd
defiantnerd deleted the compound-bind-step branch August 24, 2026 10:58
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