Skip to main content

tui_lipan/widgets/slider/
mod.rs

1//! Slider widget.
2
3mod layout;
4mod node;
5mod reconcile;
6
7pub use layout::measure_slider;
8pub use node::SliderNode;
9pub use reconcile::reconcile_slider;
10
11use unicode_width::UnicodeWidthStr;
12
13use crate::callback::{Callback, KeyHandler};
14use crate::core::element::Element;
15use crate::style::{Length, Padding, Style, StyleSlot};
16use crate::utils::gradient::ColorGradient;
17
18/// A slider for numeric selection.
19#[derive(Clone)]
20pub struct Slider {
21    /// Current value.
22    pub value: f64,
23    /// Minimum value.
24    pub min: f64,
25    /// Maximum value.
26    pub max: f64,
27    /// Step size.
28    pub step: f64,
29    /// Callback when value changes.
30    pub on_change: Option<Callback<f64>>,
31    /// Callback when clicked.
32    pub on_click: Option<Callback<f64>>,
33    /// Focused key handler. Returning `true` consumes the key before default stepping.
34    pub on_key: Option<KeyHandler>,
35    /// Track style.
36    pub style: Style,
37    /// Style for the filled portion of the track.
38    pub filled_track_style: Style,
39    /// Optional gradient for the filled portion of the track (left -> right).
40    pub filled_track_gradient: Option<ColorGradient>,
41    /// Thumb style.
42    pub thumb_style: Style,
43    /// Optional gradient for thumb color based on current value.
44    pub thumb_gradient: Option<ColorGradient>,
45    /// Label text.
46    pub label: Option<String>,
47    /// Label style.
48    pub label_style: Style,
49    /// Whether to show value text.
50    pub show_value: bool,
51    /// Requested width.
52    /// Default: `Length::Flex(1)`.
53    pub width: Length,
54    /// Requested height.
55    /// Default: `Length::Px(1)`.
56    pub height: Length,
57    /// Padding.
58    /// Default: `Padding::default()`.
59    pub padding: Padding,
60    /// Whether the slider is disabled.
61    pub disabled: bool,
62    /// Style when disabled.
63    pub disabled_style: Style,
64    /// Whether the slider is focusable.
65    pub focusable: bool,
66    /// Whether the slider participates in sequential focus navigation.
67    pub tab_stop: bool,
68    /// Callback fired when the slider receives focus.
69    pub on_focus: Option<Callback<()>>,
70    /// Callback fired when the slider loses focus.
71    pub on_blur: Option<Callback<()>>,
72    /// Hover style for the track.
73    pub hover_style: StyleSlot,
74    /// Style when focused.
75    pub focus_style: StyleSlot,
76    /// Thumb style when focused.
77    pub focus_thumb_style: StyleSlot,
78    /// Style for the thumb when hovered.
79    pub hover_thumb_style: StyleSlot,
80    /// Symbol for the thumb.
81    pub thumb_symbol: String,
82    /// Symbol for the unfilled track.
83    pub track_symbol: String,
84    /// Symbol for the filled track.
85    pub filled_track_symbol: String,
86    /// Symbol for the thumb when hovered.
87    pub hover_thumb_symbol: Option<String>,
88}
89
90impl Slider {
91    /// Create a new slider.
92    pub fn new(value: f64) -> Self {
93        Self {
94            value,
95            min: 0.0,
96            max: 100.0,
97            step: 1.0,
98            on_change: None,
99            on_click: None,
100            on_key: None,
101            style: Style::default(),
102            filled_track_style: Style::default(),
103            filled_track_gradient: None,
104            thumb_style: Style::default(),
105            thumb_gradient: None,
106            label: None,
107            label_style: Style::default(),
108            show_value: true,
109            width: Length::Flex(1),
110            height: Length::Px(1),
111            padding: Padding::default(),
112            disabled: false,
113            disabled_style: Style::default(),
114            focusable: true,
115            tab_stop: true,
116            on_focus: None,
117            on_blur: None,
118            hover_style: StyleSlot::Inherit,
119            focus_style: StyleSlot::Inherit,
120            focus_thumb_style: StyleSlot::Inherit,
121            hover_thumb_style: StyleSlot::Inherit,
122            thumb_symbol: "●".to_string(),
123            track_symbol: "─".to_string(),
124            filled_track_symbol: "━".to_string(),
125            hover_thumb_symbol: None,
126        }
127    }
128
129    /// Set minimum value.
130    pub fn min(mut self, min: f64) -> Self {
131        self.min = min;
132        self
133    }
134
135    /// Set maximum value.
136    pub fn max(mut self, max: f64) -> Self {
137        self.max = max;
138        self
139    }
140
141    /// Set step value.
142    pub fn step(mut self, step: f64) -> Self {
143        self.step = step;
144        self
145    }
146
147    /// Set label.
148    pub fn label(mut self, label: impl Into<String>) -> Self {
149        self.label = Some(label.into());
150        self
151    }
152
153    /// Set on-change callback.
154    pub fn on_change(mut self, cb: Callback<f64>) -> Self {
155        self.on_change = Some(cb);
156        self
157    }
158
159    /// Set on-click callback.
160    pub fn on_click(mut self, cb: Callback<f64>) -> Self {
161        self.on_click = Some(cb);
162        self
163    }
164
165    /// Set focused key handler. Returning `true` consumes the key before default stepping.
166    pub fn on_key(mut self, handler: KeyHandler) -> Self {
167        self.on_key = Some(handler);
168        self
169    }
170
171    /// Set label style.
172    pub fn label_style(mut self, style: Style) -> Self {
173        self.label_style = style;
174        self
175    }
176
177    /// Set whether to show value text.
178    pub fn show_value(mut self, show: bool) -> Self {
179        self.show_value = show;
180        self
181    }
182
183    /// Set style for the track.
184    pub fn style(mut self, style: Style) -> Self {
185        self.style = style;
186        self
187    }
188
189    /// Set style for the filled portion of the track.
190    pub fn filled_track_style(mut self, style: Style) -> Self {
191        self.filled_track_style = style;
192        self
193    }
194
195    /// Set gradient for the filled portion of the track.
196    pub fn filled_track_gradient(mut self, gradient: ColorGradient) -> Self {
197        self.filled_track_gradient = Some(gradient);
198        self
199    }
200
201    /// Set style for the thumb.
202    pub fn thumb_style(mut self, style: Style) -> Self {
203        self.thumb_style = style;
204        self
205    }
206
207    /// Set gradient for the thumb based on current value.
208    pub fn thumb_gradient(mut self, gradient: ColorGradient) -> Self {
209        self.thumb_gradient = Some(gradient);
210        self
211    }
212
213    /// Set requested width.
214    pub fn width(mut self, width: Length) -> Self {
215        self.width = width;
216        self
217    }
218
219    /// Set requested height.
220    pub fn height(mut self, height: Length) -> Self {
221        self.height = height;
222        self
223    }
224
225    /// Set disabled state.
226    pub fn disabled(mut self, disabled: bool) -> Self {
227        self.disabled = disabled;
228        self
229    }
230
231    /// Set disabled style.
232    pub fn disabled_style(mut self, style: Style) -> Self {
233        self.disabled_style = style;
234        self
235    }
236
237    /// Set whether the slider is focusable.
238    pub fn focusable(mut self, focusable: bool) -> Self {
239        self.focusable = focusable;
240        self
241    }
242
243    /// Control whether the slider participates in sequential focus navigation.
244    pub fn tab_stop(mut self, tab_stop: bool) -> Self {
245        self.tab_stop = tab_stop;
246        self
247    }
248
249    /// Set the callback fired when the slider receives focus.
250    pub fn on_focus(mut self, cb: Callback<()>) -> Self {
251        self.on_focus = Some(cb);
252        self
253    }
254
255    /// Set the callback fired when the slider loses focus.
256    pub fn on_blur(mut self, cb: Callback<()>) -> Self {
257        self.on_blur = Some(cb);
258        self
259    }
260
261    /// Set hover style for the track.
262    pub fn hover_style(mut self, style: Style) -> Self {
263        self.hover_style = StyleSlot::Replace(style);
264        self
265    }
266
267    /// Extend the themed hover style with the given style.
268    pub fn extend_hover_style(mut self, style: Style) -> Self {
269        self.hover_style = StyleSlot::Extend(style);
270        self
271    }
272
273    /// Inherit hover style from the active theme.
274    pub fn inherit_hover_style(mut self) -> Self {
275        self.hover_style = StyleSlot::Inherit;
276        self
277    }
278
279    /// Set the hover style slot directly.
280    pub fn hover_style_slot(mut self, slot: StyleSlot) -> Self {
281        self.hover_style = slot;
282        self
283    }
284
285    /// Set style when focused.
286    pub fn focus_style(mut self, style: Style) -> Self {
287        self.focus_style = StyleSlot::Replace(style);
288        self
289    }
290
291    /// Extend the themed focus style with the given style.
292    pub fn extend_focus_style(mut self, style: Style) -> Self {
293        self.focus_style = StyleSlot::Extend(style);
294        self
295    }
296
297    /// Inherit focus style from the active theme.
298    pub fn inherit_focus_style(mut self) -> Self {
299        self.focus_style = StyleSlot::Inherit;
300        self
301    }
302
303    /// Set the focus style slot directly.
304    pub fn focus_style_slot(mut self, slot: StyleSlot) -> Self {
305        self.focus_style = slot;
306        self
307    }
308
309    /// Set thumb style when focused.
310    pub fn focus_thumb_style(mut self, style: Style) -> Self {
311        self.focus_thumb_style = StyleSlot::Replace(style);
312        self
313    }
314
315    /// Extend the themed focused thumb style with the given style.
316    pub fn extend_focus_thumb_style(mut self, style: Style) -> Self {
317        self.focus_thumb_style = StyleSlot::Extend(style);
318        self
319    }
320
321    /// Inherit focused thumb style from the active theme.
322    pub fn inherit_focus_thumb_style(mut self) -> Self {
323        self.focus_thumb_style = StyleSlot::Inherit;
324        self
325    }
326
327    /// Set the focused thumb style slot directly.
328    pub fn focus_thumb_style_slot(mut self, slot: StyleSlot) -> Self {
329        self.focus_thumb_style = slot;
330        self
331    }
332
333    /// Set thumb style when hovered.
334    pub fn hover_thumb_style(mut self, style: Style) -> Self {
335        self.hover_thumb_style = StyleSlot::Replace(style);
336        self
337    }
338
339    /// Extend the themed hovered thumb style with the given style.
340    pub fn extend_hover_thumb_style(mut self, style: Style) -> Self {
341        self.hover_thumb_style = StyleSlot::Extend(style);
342        self
343    }
344
345    /// Inherit hovered thumb style from the active theme.
346    pub fn inherit_hover_thumb_style(mut self) -> Self {
347        self.hover_thumb_style = StyleSlot::Inherit;
348        self
349    }
350
351    /// Set the hovered thumb style slot directly.
352    pub fn hover_thumb_style_slot(mut self, slot: StyleSlot) -> Self {
353        self.hover_thumb_style = slot;
354        self
355    }
356
357    /// Set thumb symbol.
358    pub fn thumb_symbol(mut self, symbol: impl Into<String>) -> Self {
359        self.thumb_symbol = symbol.into();
360        self
361    }
362
363    /// Set track symbol.
364    pub fn track_symbol(mut self, symbol: impl Into<String>) -> Self {
365        self.track_symbol = symbol.into();
366        self
367    }
368
369    /// Set filled track symbol.
370    pub fn filled_track_symbol(mut self, symbol: impl Into<String>) -> Self {
371        self.filled_track_symbol = symbol.into();
372        self
373    }
374
375    /// Set thumb symbol when hovered.
376    pub fn hover_thumb_symbol(mut self, symbol: impl Into<String>) -> Self {
377        self.hover_thumb_symbol = Some(symbol.into());
378        self
379    }
380}
381
382impl From<Slider> for Element {
383    fn from(mut slider: Slider) -> Self {
384        // Validate and fix inverted ranges to prevent NaN/Inf in layout and rendering.
385        if slider.min > slider.max {
386            std::mem::swap(&mut slider.min, &mut slider.max);
387        }
388        // Ensure min != max to avoid division by zero.
389        if (slider.max - slider.min).abs() < f64::EPSILON {
390            slider.max = slider.min + 1.0;
391        }
392        // Clamp value to valid range.
393        slider.value = slider.value.clamp(slider.min, slider.max);
394        Element::new(crate::core::element::ElementKind::Slider(slider))
395    }
396}
397
398pub(crate) fn value_slot_width(min: f64, max: f64) -> u16 {
399    let min_text = format!("{:.1}", min);
400    let max_text = format!("{:.1}", max);
401    let width = UnicodeWidthStr::width(min_text.as_str())
402        .max(UnicodeWidthStr::width(max_text.as_str()))
403        .min(u16::MAX as usize) as u16;
404    width.max(1)
405}
406
407impl crate::layout::hash::LayoutHash for Slider {
408    fn layout_hash(
409        &self,
410        hasher: &mut impl std::hash::Hasher,
411        _recurse: &dyn Fn(&crate::core::element::Element) -> Option<u64>,
412    ) -> Option<()> {
413        use std::hash::Hash;
414        self.width.hash(hasher);
415        self.height.hash(hasher);
416        self.label.hash(hasher);
417        self.padding.hash(hasher);
418        Some(())
419    }
420}