Skip to main content

egui_components/
slider.rs

1//! `Slider` widget.
2//!
3//! egui ships its own [`egui::Slider`], but it has a very different visual
4//! identity. This widget paints a flat track + circular thumb to match the
5//! gpui-component look. Use [`egui::Slider`] if you need text input alongside,
6//! ticks, or logarithmic scales.
7
8use egui::{pos2, vec2, Color32, Response, Sense, Stroke, Ui, Widget};
9use egui_components_theme::Theme;
10
11pub struct Slider<'a> {
12    value: &'a mut f32,
13    range: std::ops::RangeInclusive<f32>,
14    width: f32,
15    disabled: bool,
16}
17
18impl<'a> Slider<'a> {
19    pub fn new(value: &'a mut f32, range: std::ops::RangeInclusive<f32>) -> Self {
20        Self { value, range, width: 200.0, disabled: false }
21    }
22    pub fn width(mut self, w: f32) -> Self {
23        self.width = w;
24        self
25    }
26    pub fn disabled(mut self, d: bool) -> Self {
27        self.disabled = d;
28        self
29    }
30}
31
32impl<'a> Widget for Slider<'a> {
33    fn ui(self, ui: &mut Ui) -> Response {
34        let theme = Theme::get(ui.ctx());
35        let m = theme.metrics;
36        let c = theme.colors;
37        let height = (m.slider_thumb_radius * 2.0).max(m.slider_track_height) + 4.0;
38        let desired = vec2(self.width, height);
39
40        let sense = if self.disabled { Sense::hover() } else { Sense::click_and_drag() };
41        let (rect, mut response) = ui.allocate_exact_size(desired, sense);
42
43        let (min, max) = (*self.range.start(), *self.range.end());
44        let track_y = rect.center().y;
45        let track_left = rect.left() + m.slider_thumb_radius;
46        let track_right = rect.right() - m.slider_thumb_radius;
47        let track_w = track_right - track_left;
48
49        if !self.disabled {
50            if let Some(pointer) = response.interact_pointer_pos() {
51                let t = ((pointer.x - track_left) / track_w).clamp(0.0, 1.0);
52                let new = min + t * (max - min);
53                if (new - *self.value).abs() > f32::EPSILON {
54                    *self.value = new;
55                    response.mark_changed();
56                }
57            }
58        }
59
60        if ui.is_rect_visible(rect) {
61            let t = ((*self.value - min) / (max - min)).clamp(0.0, 1.0);
62            let thumb_x = track_left + t * track_w;
63
64            let track_rect = egui::Rect::from_min_max(
65                pos2(track_left, track_y - m.slider_track_height * 0.5),
66                pos2(track_right, track_y + m.slider_track_height * 0.5),
67            );
68            let track_radius =
69                egui::CornerRadius::same((m.slider_track_height * 0.5) as u8);
70
71            let painter = ui.painter();
72
73            let track_bg = if self.disabled {
74                fade(c.muted_background)
75            } else {
76                c.muted_background
77            };
78            painter.rect_filled(track_rect, track_radius, track_bg);
79
80            // Filled portion
81            let filled = egui::Rect::from_min_max(track_rect.min, pos2(thumb_x, track_rect.max.y));
82            let fill_color = if self.disabled { fade(c.slider_bar_background) } else { c.slider_bar_background };
83            painter.rect_filled(filled, track_radius, fill_color);
84
85            // Thumb
86            let thumb_color = if self.disabled { fade(c.slider_thumb_background) } else { c.slider_thumb_background };
87            let thumb_border = if self.disabled { fade(c.slider_bar_background) } else { c.slider_bar_background };
88            painter.circle(
89                pos2(thumb_x, track_y),
90                m.slider_thumb_radius,
91                thumb_color,
92                Stroke::new(2.0, thumb_border),
93            );
94
95            if response.has_focus() {
96                painter.circle_stroke(
97                    pos2(thumb_x, track_y),
98                    m.slider_thumb_radius + 3.0,
99                    theme.focus_ring(),
100                );
101            }
102
103            if !self.disabled && response.hovered() {
104                ui.ctx().set_cursor_icon(egui::CursorIcon::Grab);
105            }
106        }
107
108        response
109    }
110}
111
112fn fade(c: Color32) -> Color32 {
113    egui_components_theme::mix(c, Color32::from_rgba_unmultiplied(0, 0, 0, 0), 0.4)
114}