Skip to main content

Module spin_box

Module spin_box 

Source
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_text is handed every character). The value is committed on Enter or on focus loss — at commit time the text is parsed, clamped into [min, max] (or wrapped, per WrapMode), 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 as Enter would read it, and the step moves on from there: type 35, press Up with 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 is QAbstractSpinBox::stepBy.
  • Keyboard:
    • Up / Down → ±single_step
    • PageUp / PageDown → ±page_step (default: 10 × single_step)
    • Enter → commit (stays focused)
    • Home / End stay bound to the text cursor. QAbstractSpinBox routes both to its inner QLineEdit, and WinUI’s NumberBox, Blink, Avalonia and jQuery UI bind neither; typing the number reaches min and max anyway. See docs/range-keyboard.md.
    • A chord holding Ctrl, Alt or Super is not the spin box’s and falls through to the application; Shift does not change the step.
  • Mouse wheel: adjusts by single_step — wheel down decreases, wheel up increases, matching QAbstractSpinBox, GtkSpinButton and WinUI’s NumberBox. Gated by wheel_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 min and special_value_text is 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’s AdaptiveDecimalStepType). 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 one NumberPresentation, which a live language switch replaces, so they cannot disagree about which separator the field is using: a French user sees 12,5, types 12,5, and the numeric keypad’s . still works. Rendering is a string transform over the value’s own Display, never an f64 round-trip, so a SpinBox<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 no I18nManager installed the active locale is the C locale and this is a no-op.
  • Custom formatter / parser: full override via text_from_value and value_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 for i32, i64, u32, u64, usize, f32, and f64.

Enums§

ButtonLayout
Step-button visibility / placement. Moved up from teksilo_widgets::spin_box::ButtonLayout so the trait config can carry it without forcing the recipe to depend on the widget crate.
StepType
Step-size policy for each key/button press.
WheelMode
When the mouse wheel is allowed to adjust the value.
WidthPolicy
How the SpinBox decides its horizontal size envelope.
WrapMode
Out-of-range behavior when stepping past min or max.

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.

Traits§

SpinValue
Numeric primitive that a SpinBox can hold.