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(div().text_color(value_color).child(format!("{:.1}", self.value)));
236            }
237
238            container = container.child(label_row);
239        }
240
241        // Slider track
242        let on_change = self.on_change;
243        let mut track = div()
244            .id(self.id)
245            .w(px(width))
246            .h(px(thumb_size))
247            .flex()
248            .items_center()
249            .relative()
250            // Track background
251            .child(
252                div()
253                    .absolute()
254                    .left_0()
255                    .w_full()
256                    .h(px(track_height))
257                    .rounded(px(track_height / 2.0))
258                    .bg(track_color),
259            )
260            // Fill
261            .child(
262                div()
263                    .absolute()
264                    .left_0()
265                    .w(px(fill_width))
266                    .h(px(track_height))
267                    .rounded(px(track_height / 2.0))
268                    .bg(if disabled {
269                        rgba(0xccccccff)
270                    } else {
271                        fill_color
272                    }),
273            )
274            // Thumb
275            .child(
276                div()
277                    .absolute()
278                    .left(px(thumb_left.max(0.0)))
279                    .w(px(thumb_size))
280                    .h(px(thumb_size))
281                    .rounded_full()
282                    .bg(thumb_color)
283                    .border_2()
284                    .border_color(if disabled {
285                        rgba(0xccccccff)
286                    } else {
287                        fill_color
288                    })
289                    .shadow_sm(),
290            );
291
292        // Apply cursor style
293        if disabled {
294            track = track.cursor_not_allowed();
295        } else {
296            track = track.cursor_pointer();
297        }
298
299        // Add click handling if not disabled and has callback
300        if !disabled {
301            if let Some(handler) = on_change {
302                let handler_ptr: *const dyn Fn(f32, &mut Window, &mut App) = handler.as_ref();
303                track = track.on_mouse_down(MouseButton::Left, move |event, window, cx| {
304                    // Get relative x position within the slider
305                    // Note: event.position is in window coordinates, we need to calculate ratio differently
306                    // For now, we'll use a simpler approach with the bounds
307                    let click_x = event.position.x;
308                    // We need to get the element bounds, but for simplicity we'll estimate
309                    // This is a simplified version - a more complete implementation would track bounds
310                    let ratio = ((click_x - px(0.0)) / px(width)).clamp(0.0, 1.0);
311                    let new_value = min + ratio * (max - min);
312                    let snapped = if let Some(step) = step {
313                        let steps = ((new_value - min) / step).round();
314                        (min + steps * step).clamp(min, max)
315                    } else {
316                        new_value
317                    };
318                    // SAFETY: handler lives as long as the closure
319                    unsafe { (*handler_ptr)(snapped, window, cx) };
320                });
321                std::mem::forget(handler);
322            }
323        }
324
325        container.child(track)
326    }
327}