Expand description
SpinBox — numeric input with increment/decrement buttons.
A generic composite over SpinValue
(integer and floating-point primitives), pairing the
TextInputField editing
primitive with a stacked pair of up/down step buttons. Semantics
are a synthesis of Qt’s QSpinBox / QDoubleSpinBox, WinUI 3’s
NumberBox, GTK’s GtkSpinButton, and the W3C ARIA
spinbutton role.
§Behaviour
- Value binding: a
Signal<T>is the single source of truth. Typing and stepping update it; external writes re-format the editable text. - Commit model: the user can type freely (subject to the
per-character input filter, which only the default parser has:
a custom
value_from_textis handed every character). The value is committed onEnteror on focus loss — at commit time the text is parsed, clamped into[min, max](or wrapped, perWrapMode), and reformatted. Invalid input reverts to the last known good value. - Stepping over typed text: every step (key, wheel, button,
assistive
Increment/Decrement) starts from what the field shows. Text typed and not yet committed is read first, exactly asEnterwould read it, and the step moves on from there: type 35, pressUpwith a step of 5, and the value is 40, reported once. Text that cannot be read steps nothing; the value goes back into the field, and the next press steps from it. This isQAbstractSpinBox::stepBy. - Keyboard:
Up/Down→ ±single_stepPageUp/PageDown→ ±page_step(default:10 × single_step)Enter→ commit (stays focused)Home/Endstay bound to the text cursor.QAbstractSpinBoxroutes both to its innerQLineEdit, and WinUI’sNumberBox, Blink, Avalonia and jQuery UI bind neither; typing the number reaches min and max anyway. Seedocs/range-keyboard.md.- A chord holding
Ctrl,AltorSuperis not the spin box’s and falls through to the application;Shiftdoes not change the step.
- Mouse wheel: adjusts by
single_step— wheel down decreases, wheel up increases, matchingQAbstractSpinBox,GtkSpinButtonand WinUI’sNumberBox. Gated bywheel_mode(default: only when focused, to avoid accidental scroll changes). - Buttons: up/down buttons stack to the right of the field
by default; can be hidden with
button_layout. - Special value text: when the current value equals
minandspecial_value_textis set, the field shows that string instead of the formatted number (Qt’s “Auto” / “None” / “Unlimited” affordance). It stays while the field has focus, as in Qt: keyboard focus selects it, so a number typed replaces it. - Adaptive step: with
StepType::Adaptive, the effective step tracks the decimal magnitude of the current value (Qt’sAdaptiveDecimalStepType). Useful for values that span many orders of magnitude in the same control. - Locale: the number follows the active locale’s decimal
separator, digits and minus sign
(
localized, on by default); thousands separators are opt-in (use_grouping, off by default, as in Qt). Display, commit parse, stepping and the per-character input filter all read oneNumberPresentation, which a live language switch replaces, so they cannot disagree about which separator the field is using: a French user sees12,5, types12,5, and the numeric keypad’s.still works. Rendering is a string transform over the value’s ownDisplay, never anf64round-trip, so aSpinBox<i64>stays exact past 2^53. Turn it off for a number that is an identifier rather than a quantity (port, version component, database id). With noI18nManagerinstalled the active locale is the C locale and this is a no-op. - Custom formatter / parser: full override via
text_from_valueandvalue_from_text; together they let you implement currency, percentages with stored fraction, hex, duration, anything. A custom formatter/parser owns the whole convention — it is not re-punctuated by the locale layer.
§Accessibility
The editing field is the spin button: one AccessKit node with
Role::SpinButton,
the label as its name, the numeric value, min,
max, step and jump, the field’s text as its value and its text
runs, and the
Increment,
Decrement,
SetValue, and
Focus actions. It is
the node that holds focus, so a focus change reports the field’s
name and value once, and a step moves the number a screen reader
is following. The composite’s own node is structure and collapses;
it names the field as its
accessibility_proxy,
so an access_label, a FormLayout label, an
access_described_by or a tooltip given to the spin box lands on
the field. The suffix is painted and not part
of the text, so it is not announced.
SetValue accepts either payload shape, because both are sent in
the field: a number (macOS setAccessibilityValue: with an
NSNumber, AT-SPI’s Value.SetCurrentValue) is clamped and
published as sent; a string (an NSString, the automation
set_value tool) takes the same parse Enter takes, so a custom
value_from_text and the locale’s
decimal separator are honoured, and an unparseable one reverts the
display and is reported unhandled. A
read_only spin box advertises and services
none of the three mutating actions. The step buttons are
structurally part of the SpinBox and publish no separate a11y
nodes; stepping is the spin button’s Increment and Decrement.
§Example
use teksilo::widgets::{SpinBox, WrapMode};
let font_size = ctx.signal(12_i32);
ctx.add(
SpinBox::new(font_size, 4, 72)
.single_step(1)
.page_step(10)
.suffix(" pt"),
);
let gain_db = ctx.signal(0.0_f32);
ctx.add(
SpinBox::new(gain_db, -60.0, 12.0)
.single_step(0.5)
.decimals(1)
.suffix(" dB")
.wrap_mode(WrapMode::Clamp),
);§Touch and pen
The step buttons are the controls sweep’s one unreachable target, and
the reason is recorded rather than papered over. Each is 18 x 13 dp inside a
trailing column exactly its own width and exactly two buttons tall, so a
Widget::hit_outset has nowhere to grow (an outset never escapes its
parent); the field beside them takes presses of its own, so the miss-only
slop pass has an eligible bubble owner at distance zero; and two conforming
targets stacked need 88 dp of column, which no density projection of the
field produces. Reaching the floor here is a layout change — the desktop
stacked pair replaced by a side-by-side −/+ at coarse densities, as Material
does — and that is a design decision, not a targeting one. The value stays
fully reachable by keyboard (Up/Down, PageUp/PageDown) and by the
Increment / Decrement assistive actions.
One thing the sweep did fix. A step still fires on the press and arms
hold-to-repeat from it (Qt’s QAbstractSpinBox convention — there is no
release to start a repeat from), but the repeat now stops on a
PointerCancel: a pan claimant winning the press used to leave the box
stepping for the rest of the session, because a cancel is terminal and no
PointerUp follows it.
What the box does about panning, it does by omission. It declares no
touch_action and makes no pan claim, so its subtree keeps the default
TouchAction::AUTO and a finger that comes to rest on it and then drags
is won by the enclosing scroller rather than changing the value. The
on_scroll handler below is a wheel handler and not a pan; it never sees
a finger.
Two tests in spin_box/tests.rs cover that, and only one of them can see
the second half of it. a_finger_pan_over_the_spin_box_scrolls_its_container
shows the pan reaching the scroller, but its box has the default
WheelMode::Focused and its fixture never focuses the field, so the
on_scroll below declines before the pan question is reached — a pan that
DID arrive would leave the value unchanged there anyway.
a_finger_pan_over_a_hover_wheel_spin_box_scrolls_its_container is the one
that can observe the claim: with WheelMode::Hover the handler runs for
every scroll that reaches the box, so a claim would step the value once per
synthesised sample.
P22, the scrollables migration, looked at the explicit
touch_action(PAN_Y) this file was asked for and decided against it.
It is a narrowing on top of the behaviour above rather than a restatement
of it: it would additionally forbid a horizontal pan and a pinch through
the field — a real cost to a spin box in a horizontally-scrolling toolbar,
or on a pinch-zoomable page — and it would buy nothing the omission does
not already give, because the claimant chain visits only the pan candidates
and never the generic bubble, so a boundary pan cannot reach the on_scroll
below however far it travels. The omission is the decision; the
hover-wheel test named above is what can witness it. P24 owns
spin_box/step_button.rs.
Structs§
- SpinBox
- Numeric input with step buttons. Generic over
SpinValue— pre-implemented fori32,i64,u32,u64,usize,f32, andf64.
Enums§
- Button
Layout - Step-button visibility / placement. Moved up from
teksilo_widgets::spin_box::ButtonLayoutso the trait config can carry it without forcing the recipe to depend on the widget crate. - Step
Type - Step-size policy for each key/button press.
- Wheel
Mode - When the mouse wheel is allowed to adjust the value.
- Width
Policy - How the SpinBox decides its horizontal size envelope.
- Wrap
Mode - Out-of-range behavior when stepping past
minormax.
Constants§
- SPIN_
BOX_ STEP_ BUTTON_ HEIGHT - Painted height of one stacked step button, in dp. Only a fallback — the
real height is half the field’s inner height (see
build_step_buttons). - SPIN_
BOX_ STEP_ BUTTON_ WIDTH - Minimum total width. Below this the stacked step buttons stop
fitting next to the field. Widgets narrower than this are
enforced to the minimum at layout time via
MinSize. Painted width of one stacked step button, in dp.