teksilo_core/styles/spin_box_style.rs
1// SPDX-License-Identifier: MPL-2.0
2// SPDX-FileCopyrightText: 2026 FernTech
3
4//! Tier-3 style protocol for `SpinBox`. See `docs/styling-system.md`.
5//!
6//! `SpinBox` composes a themed `TextInputField` with an optional pair
7//! of step-up / step-down buttons (each themed via `IconButtonStyle`
8//! at construction time inside `StepButton`). The remaining picker-
9//! specific chrome is the *arrangement*: where the field sits, where
10//! the buttons sit, and the focus-aware border that frames them as
11//! one control.
12//!
13//! `make_body` receives the pre-built field plus optional step buttons
14//! and returns the visual core (the bordered surface holding both).
15//! The widget keeps responsibility for sizing policy (`width_chars` /
16//! `width_pixels`), keyboard / wheel handlers, and the `Role::SpinButton`
17//! accessibility.
18
19use std::rc::Rc;
20
21use crate::build_context::BuildContext;
22use crate::signal::Signal;
23use crate::widget_id::WidgetId;
24
25/// Step-button visibility / placement. Moved up from
26/// `teksilo_widgets::spin_box::ButtonLayout` so the trait config can carry
27/// it without forcing the recipe to depend on the widget crate.
28#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
29pub enum ButtonLayout {
30 /// Up arrow on top, down arrow below, stacked vertically to the
31 /// right of the field. Default, matches Qt and WinUI.
32 #[default]
33 Stacked,
34 /// No visible step buttons. Useful for read-only displays and
35 /// for SpinBoxes driven entirely by keyboard / wheel.
36 Hidden,
37}
38
39pub struct SpinBoxStyleConfig {
40 /// Pre-built `TextInputField` subtree (already padded to the
41 /// caller's policy).
42 pub field: WidgetId,
43 /// Pre-built up-step icon button. `None` when `layout == Hidden`.
44 pub step_up: Option<WidgetId>,
45 /// Pre-built down-step icon button. `None` when `layout == Hidden`.
46 pub step_down: Option<WidgetId>,
47 /// Layout selector — `Stacked` puts the buttons in a divided
48 /// vertical column to the trailing side of the field; `Hidden`
49 /// drops the divider and button column entirely.
50 pub layout: ButtonLayout,
51 /// Reactive focus signal — drives the border colour (focused →
52 /// accent, otherwise default).
53 pub is_focused: Signal<bool>,
54 /// Reactive disabled signal — the AND of the SpinBox's own `enabled`
55 /// prop and every ancestor's, via
56 /// `BuildContext::effective_enabled_signal`. Drives the inert grey
57 /// fill / outline. A SpinBox frames a `TextInputField` in *neutral*
58 /// roles (`SurfaceRole::Content`, `BorderRole::Default`), and the
59 /// disabled-role substitution in `ColorProp::resolve` only rewrites
60 /// the *accent* family — so unlike an accent-filled Button, this
61 /// control has to dim itself explicitly.
62 pub is_disabled: Signal<bool>,
63}
64
65pub trait SpinBoxStyle: 'static {
66 fn make_body(&self, cfg: &SpinBoxStyleConfig, ctx: &mut BuildContext) -> WidgetId;
67}
68
69pub type SharedSpinBoxStyle = Rc<dyn SpinBoxStyle>;