PixelUI keeps numeric state, editing transactions, presentation, and drawing separate. The shared layer uses int32_t values, fixed-capacity storage, and integer arithmetic.
Range and normalization
Create a NumericRange through its validating factory:
1 | NumericRange range; |
clamp(), canIncrement(), canDecrement(), incremented(), and decremented() are saturating. A final step that does not divide the range exactly lands on the boundary. normalizeToExtent(range, value, pixels) maps a clamped value to an integer pixel extent with nearest-integer rounding. These operations are safe across the full INT32_MIN to INT32_MAX range.
tryCreate() returns false for minimum > maximum or step <= 0 and leaves the output object unchanged. Check that result before retaining or passing the range.
Formatting
NumericFormatter formats the value passed to it; it does not own or read application state. Built-in policies cover integers, zero padding, scaled integers, suffixes, and percentages:
1 | IntegerFormat padded{3, nullptr}; |
Formatting is allocation-free. format() returns false when the formatter is invalid or the destination is too small; a non-empty destination is reset to an empty string on failure. Use FixedBufferWriter::appendInteger() when composing text such as current/total instead of introducing a separate conversion routine.
The formatter stores only a function pointer and a non-owning context pointer. Every referenced format object, suffix string, percentage range, or custom context must outlive all calls through that formatter. A local formatter is safe for immediate formatting; a formatter retained by a Widget or queued Popup must refer to static storage or persistent App members.
Declare retained dependencies before their formatter so reverse member destruction keeps them alive:
1 | NumericRange temperatureRange_{}; |
Binding and edit session
ValueEditorBinding is only a non-owning read/write/notification boundary. ValueEditSession owns the transaction state: its original value, draft value, and policy.
1 | static void changed(void* context, int32_t value) { |
With CommitOnConfirm, draft changes remain inside the session until commit() writes and notifies; cancel() discards the draft. With Live, each successful setDraftValue() writes and notifies immediately; cancel() restores the original value and notifies that restoration. A session constructed from an initial integer has no external binding but follows the same draft/commit/cancel path.
| Operation | CommitOnConfirm |
Live |
|---|---|---|
setDraftValue(value) |
Update only the draft | Write, notify, then update the draft |
commit() |
Write and notify if changed; make draft the new original | Make the current draft the new original |
cancel() |
Discard the draft | Restore and notify the original value if changed |
All three operations return false when the session is invalid or a required write fails. A failed write does not advance the transaction state; the caller should keep the editor open or restore its visible controls from draftValue().
Binding value and changed contexts are non-owning and must outlive the session and every operation performed through it. A Popup request that copies the binding extends this requirement through both its pending and active lifetime.
Component responsibilities
NumScrollusesNumericRangefor stepping andNumericFormatterfor labels. UsesetRange(min, max, step)orsetRange(range), andsetFormatter(formatter). Zero padding is a formatter policy;setFixedIntDigits()no longer exists.PopupProgressmaps LEFT/RIGHT throughrange.step(), normalizes only the bar geometry, and delegates displayed text to the formatter.PopupValueDigitsuses aValueEditSession; construction does not modify the external value. SELECT on a digit only finishes that digit’s local edit, allowing other digits to be changed. The OK button commits the whole session; CANCEL or BACK cancels it.


