gpui_ui_kit/
slider.rs

1//! Slider component for selecting numeric values within a range
2
3use gpui::*;
4
5/// Theme colors for slider styling
6#[derive(Debug, Clone)]
7pub struct SliderTheme {
8    /// Track background color (unfilled portion)
9    pub track: Rgba,
10    /// Fill color (active portion)
11    pub fill: Rgba,
12    /// Thumb/handle color
13    pub thumb: Rgba,
14    /// Label text color
15    pub label: Rgba,
16    /// Value text color
17    pub value: Rgba,
18}
19
20impl Default for SliderTheme {
21    fn default() -> Self {
22        Self {
23            track: rgba(0x3e3e3eff),
24            fill: rgba(0x007accff),
25            thumb: rgba(0xffffffff),
26            label: rgba(0xccccccff),
27            value: rgba(0x999999ff),
28        }
29    }
30}
31
32/// Slider size variants
33#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
34pub enum SliderSize {
35    Small,
36    #[default]
37    Medium,
38    Large,
39}
40
41impl SliderSize {
42    fn track_height(&self) -> f32 {
43        match self {
44            Self::Small => 4.0,
45            Self::Medium => 6.0,
46            Self::Large => 8.0,
47        }
48    }
49
50    fn thumb_size(&self) -> f32 {
51        match self {
52            Self::Small => 14.0,
53            Self::Medium => 18.0,
54            Self::Large => 22.0,
55        }
56    }
57}
58
59/// A slider component for selecting numeric values
60#[derive(IntoElement)]
61pub struct Slider {
62    id: ElementId,
63    value: f32,
64    min: f32,
65    max: f32,
66    step: Option<f32>,
67    size: SliderSize,
68    disabled: bool,
69    show_value: bool,
70    label: Option<SharedString>,
71    width: f32,
72    on_change: Option<Box<dyn Fn(f32, &mut Window, &mut App) + 'static>>,
73    track_color: Option<Rgba>,
74    fill_color: Option<Rgba>,
75    thumb_color: Option<Rgba>,
76    theme: Option<SliderTheme>,
77}
78
79impl Slider {
80    /// Create a new slider with the given ID
81    pub fn new(id: impl Into<ElementId>) -> Self {
82        Self {
83            id: id.into(),
84            value: 0.0,
85            min: 0.0,
86            max: 100.0,
87            step: None,
88            size: SliderSize::default(),
89            disabled: false,
90            show_value: false,
91            label: None,
92            width: 200.0,
93            on_change: None,
94            track_color: None,
95            fill_color: None,
96            thumb_color: None,
97            theme: None,
98        }
99    }
100
101    /// Set the current value
102    pub fn value(mut self, value: f32) -> Self {
103        self.value = value.clamp(self.min, self.max);
104        self
105    }
106
107    /// Set the minimum value
108    pub fn min(mut self, min: f32) -> Self {
109        self.min = min;
110        self
111    }
112
113    /// Set the maximum value
114    pub fn max(mut self, max: f32) -> Self {
115        self.max = max;
116        self
117    }
118
119    /// Set the step size for snapping
120    pub fn step(mut self, step: f32) -> Self {
121        self.step = Some(step);
122        self
123    }
124
125    /// Set the slider size
126    pub fn size(mut self, size: SliderSize) -> Self {
127        self.size = size;
128        self
129    }
130
131    /// Set disabled state
132    pub fn disabled(mut self, disabled: bool) -> Self {
133        self.disabled = disabled;
134        self
135    }
136
137    /// Show the current value as text
138    pub fn show_value(mut self, show: bool) -> Self {
139        self.show_value = show;
140        self
141    }
142
143    /// Set a label for the slider
144    pub fn label(mut self, label: impl Into<SharedString>) -> Self {
145        self.label = Some(label.into());
146        self
147    }
148
149    /// Set the width of the slider in pixels
150    pub fn width(mut self, width: f32) -> Self {
151        self.width = width;
152        self
153    }
154
155    /// Set the change handler
156    pub fn on_change(mut self, handler: impl Fn(f32, &mut Window, &mut App) + 'static) -> Self {
157        self.on_change = Some(Box::new(handler));
158        self
159    }
160
161    /// Set the track color
162    pub fn track_color(mut self, color: impl Into<Rgba>) -> Self {
163        self.track_color = Some(color.into());
164        self
165    }
166
167    /// Set the fill color
168    pub fn fill_color(mut self, color: impl Into<Rgba>) -> Self {
169        self.fill_color = Some(color.into());
170        self
171    }
172
173    /// Set the thumb color
174    pub fn thumb_color(mut self, color: impl Into<Rgba>) -> Self {
175        self.thumb_color = Some(color.into());
176        self
177    }
178
179    /// Set the slider theme (applies all colors at once)
180    pub fn theme(mut self, theme: SliderTheme) -> Self {
181        self.theme = Some(theme);
182        self
183    }
184}
185
186impl RenderOnce for Slider {
187    fn render(self, _window: &mut Window, _cx: &mut App) -> impl IntoElement {
188        let track_height = self.size.track_height();
189        let thumb_size = self.size.thumb_size();
190        let width = self.width;
191
192        // Use theme colors if available, then individual colors, then defaults
193        let default_theme = SliderTheme::default();
194        let theme = self.theme.as_ref().unwrap_or(&default_theme);
195        let track_color = self.track_color.unwrap_or(theme.track);
196        let fill_color = self.fill_color.unwrap_or(theme.fill);
197        let thumb_color = self.thumb_color.unwrap_or(theme.thumb);
198        let label_color = theme.label;
199        let value_color = theme.value;
200
201        let range = self.max - self.min;
202        let progress = if range > 0.0 {
203            (self.value - self.min) / range
204        } else {
205            0.0
206        };
207
208        let fill_width = (width * progress).max(0.0);
209        let thumb_left = (width * progress) - (thumb_size / 2.0);
210
211        let min = self.min;
212        let max = self.max;
213        let step = self.step;
214        let disabled = self.disabled;
215
216        let mut container = div().flex().flex_col().gap_1();
217
218        // Label row
219        if self.label.is_some() || self.show_value {
220            let mut label_row = div().flex().justify_between().w(px(width)).text_sm();
221
222            if let Some(label) = &self.label {
223                label_row = label_row.child(
224                    div()
225                        .text_color(if disabled {
226                            rgba(0x66666699)
227                        } else {
228                            label_color
229                        })
230                        .child(label.clone()),
231                );
232            }
233
234            if self.show_value {
235                label_row = label_row.child(
236                    div()
237                        .text_color(value_color)
238                        .child(format!("{:.1}", self.value)),
239                );
240            }
241
242            container = container.child(label_row);
243        }
244
245        // Slider track
246        let on_change = self.on_change;
247        let mut track = div()
248            .id(self.id)
249            .w(px(width))
250            .h(px(thumb_size))
251            .flex()
252            .items_center()
253            .relative()
254            // Track background
255            .child(
256                div()
257                    .absolute()
258                    .left_0()
259                    .w_full()
260                    .h(px(track_height))
261                    .rounded(px(track_height / 2.0))
262                    .bg(track_color),
263            )
264            // Fill
265            .child(
266                div()
267                    .absolute()
268                    .left_0()
269                    .w(px(fill_width))
270                    .h(px(track_height))
271                    .rounded(px(track_height / 2.0))
272                    .bg(if disabled {
273                        rgba(0xccccccff)
274                    } else {
275                        fill_color
276                    }),
277            )
278            // Thumb
279            .child(
280                div()
281                    .absolute()
282                    .left(px(thumb_left.max(0.0)))
283                    .w(px(thumb_size))
284                    .h(px(thumb_size))
285                    .rounded_full()
286                    .bg(thumb_color)
287                    .border_2()
288                    .border_color(if disabled {
289                        rgba(0xccccccff)
290                    } else {
291                        fill_color
292                    })
293                    .shadow_sm(),
294            );
295
296        // Apply cursor style
297        if disabled {
298            track = track.cursor_not_allowed();
299        } else {
300            track = track.cursor_pointer();
301        }
302
303        // Add click handling if not disabled and has callback
304        // Use Rc to share handler between potential multiple calls
305        if !disabled {
306            if let Some(handler) = on_change {
307                let handler = std::rc::Rc::new(handler);
308
309                // Store current value in closure
310                let current_value = self.value;
311
312                track = track.on_mouse_down(MouseButton::Left, move |_event, window, cx| {
313                    // Since we can't reliably get element bounds in GPUI's on_mouse_down,
314                    // we implement a simple step-based behavior:
315                    // - Each click steps through values in the step direction
316                    // - The step size is determined by the step parameter or a default
317                    let step_amount = step.unwrap_or((max - min) / 10.0);
318
319                    // Cycle through: increment by step, wrap at max
320                    let new_value = current_value + step_amount;
321                    let snapped = if new_value > max {
322                        min // Wrap around to min when exceeding max
323                    } else if let Some(step) = step {
324                        let steps = ((new_value - min) / step).round();
325                        (min + steps * step).clamp(min, max)
326                    } else {
327                        new_value.clamp(min, max)
328                    };
329                    handler(snapped, window, cx);
330                });
331            }
332        }
333
334        container.child(track)
335    }
336}