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>;