Skip to main content

teksilo_core/styles/
slider_style.rs

1// SPDX-License-Identifier: MPL-2.0
2// SPDX-FileCopyrightText: 2026 FernTech
3
4//! Tier-3 style protocol for `Slider`. See `docs/styling-system.md`.
5
6use std::rc::Rc;
7
8use serde::{Deserialize, Serialize};
9
10use crate::build_context::BuildContext;
11use crate::focus::FocusOrigin;
12use crate::signal::Signal;
13use crate::widget_id::WidgetId;
14use teksilo_tokens::InputTokens;
15
16#[derive(Copy, Clone, Debug, Eq, PartialEq, Hash, Default, Serialize, Deserialize)]
17pub enum SliderVariant {
18    #[default]
19    Continuous,
20    /// Snaps to discrete tick positions; the style typically paints
21    /// the ticks above/below the track.
22    Discrete,
23    /// Two thumbs: the value is a `(low, high)` range. Here for
24    /// completeness; the IntUI default impl does NOT yet wire range
25    /// behaviour — apps that need range sliders write a custom
26    /// impl.
27    Range,
28}
29
30/// Slider orientation. Horizontal is the default.
31///
32/// A horizontal slider's value progresses along the **reading direction**: left
33/// → right in LTR, right → left in RTL. The minimum therefore sits at the
34/// leading edge on both, and the pointer axis, the painted fill and the
35/// horizontal arrow keys all mirror together. A style that paints its own track
36/// must mirror with them — `cfg.value_normalized` is the value, not a screen
37/// position.
38#[derive(Copy, Clone, Debug, Eq, PartialEq, Hash, Default, Serialize, Deserialize)]
39pub enum SliderOrientation {
40    #[default]
41    Horizontal,
42    Vertical,
43}
44
45#[derive(Clone, Debug)]
46pub struct SliderStyleConfig {
47    /// Normalized `0.0..=1.0` thumb position.
48    pub value_normalized: Signal<f32>,
49    pub is_hovered: Signal<bool>,
50    /// `true` while the user is drag-pressing the thumb.
51    pub is_dragging: Signal<bool>,
52    pub is_disabled: Signal<bool>,
53    /// `Some(FocusOrigin::Keyboard)` while the slider has keyboard
54    /// focus; the IntUI default uses this to gate the focus ring on
55    /// the thumb. `Some(Pointer)` and `None` skip the ring.
56    pub focus_origin: Signal<Option<FocusOrigin>>,
57    pub orientation: SliderOrientation,
58    /// `Some(n)` ⇒ Discrete with `n` ticks; `None` ⇒ Continuous.
59    pub tick_count: Option<u32>,
60    pub variant: SliderVariant,
61}
62
63pub trait SliderStyle: 'static {
64    /// Build the slider's chrome — track, fill and thumb.
65    ///
66    /// A horizontal slider's minimum sits at the **leading** edge, which is the
67    /// right one in a right-to-left window. The host widget mirrors its pointer
68    /// mapping and its arrow keys there, so a style that paints the fill from
69    /// the left unconditionally will disagree with both. Read
70    /// `PaintContext::layout_direction` at paint time — not at build time,
71    /// because a locale change repaints without rebuilding — and mirror the
72    /// thumb position and the fill's anchor edge. The vertical orientation has
73    /// no leading/trailing to mirror.
74    ///
75    /// This carries the same obligation as [`thumb_diameter`](Self::thumb_diameter)
76    /// below: the widget cannot enforce it, and getting it wrong is only
77    /// visible in an RTL locale.
78    fn make_body(&self, cfg: &SliderStyleConfig, ctx: &mut BuildContext) -> WidgetId;
79
80    /// Diameter, in logical pixels, of the draggable thumb produced by
81    /// `make_body`. The host `Slider` widget uses this to compute the drag
82    /// hit-region and the position→value mapping at event time (when it can
83    /// no longer reach the theme). The default matches the IntUI recipe's
84    /// thumb; a custom style that paints a different thumb size MUST override
85    /// this too, or dragging will map to the wrong pixel boundary.
86    fn thumb_diameter(&self, _cfg: &SliderStyleConfig) -> f32 {
87        14.0
88    }
89
90    /// The same diameter, told which density is active.
91    ///
92    /// The thumb is *paint geometry inside one leaf node* — the track, the
93    /// fill and the knob are one canvas — so the host `Slider` reads this at
94    /// event time to size the grab region, and it is the only place a density
95    /// can reach it. Additive and defaulted to
96    /// [`thumb_diameter`](Self::thumb_diameter), so a style written before the
97    /// density layer existed keeps working and keeps its own number; a style
98    /// that wants a bigger knob under a coarse pointer overrides this one
99    /// instead.
100    ///
101    /// The **painted** knob is deliberately not grown here: A10 gives the
102    /// slider its coarse target through `target_regions`, which reports the
103    /// knob at this diameter, over an unchanged 14 dp visual (the design's
104    /// Constants table: "14 dp visual → 24 dp hit, 44 at Touch"). This exists
105    /// so a style *may* disagree, not so the framework does.
106    fn thumb_diameter_for(&self, cfg: &SliderStyleConfig, _tokens: &InputTokens) -> f32 {
107        self.thumb_diameter(cfg)
108    }
109}
110
111pub type SharedSliderStyle = Rc<dyn SliderStyle>;