1use gpui::prelude::*;
10use gpui::{
11 div, px, relative, Context, EventEmitter, FocusHandle, IntoElement, KeyDownEvent, SharedString,
12 Window,
13};
14
15use crate::theme::{theme, ColorName, Size};
16
17#[derive(Debug, Clone, Copy)]
19pub struct SliderEvent(pub f64);
20
21pub struct Slider {
23 value: f64,
24 min: f64,
25 max: f64,
26 step: f64,
27 color: ColorName,
28 focus: FocusHandle,
29 disabled: bool,
30}
31
32impl EventEmitter<SliderEvent> for Slider {}
33
34impl Slider {
35 pub fn new(cx: &mut Context<Self>) -> Self {
36 Slider {
37 value: 0.0,
38 min: 0.0,
39 max: 100.0,
40 step: 1.0,
41 color: ColorName::Blue,
42 focus: cx.focus_handle(),
43 disabled: false,
44 }
45 }
46
47 pub fn value(mut self, value: f64) -> Self {
48 self.value = value;
49 self
50 }
51
52 pub fn min(mut self, min: f64) -> Self {
53 self.min = min;
54 self
55 }
56
57 pub fn max(mut self, max: f64) -> Self {
58 self.max = max;
59 self
60 }
61
62 pub fn step(mut self, step: f64) -> Self {
63 self.step = step.max(f64::EPSILON);
64 self
65 }
66
67 pub fn color(mut self, color: ColorName) -> Self {
68 self.color = color;
69 self
70 }
71
72 pub fn disabled(mut self, disabled: bool) -> Self {
73 self.disabled = disabled;
74 self
75 }
76
77 pub fn value_f64(&self) -> f64 {
78 self.value
79 }
80
81 fn fraction(&self) -> f32 {
82 if self.max <= self.min {
83 0.0
84 } else {
85 (((self.value - self.min) / (self.max - self.min)) as f32).clamp(0.0, 1.0)
86 }
87 }
88
89 fn snap(&self, raw: f64) -> f64 {
90 let stepped = (raw / self.step).round() * self.step;
91 stepped.clamp(self.min, self.max)
92 }
93
94 fn set_value(&mut self, raw: f64, cx: &mut Context<Self>) {
95 if self.disabled {
96 return;
97 }
98 let next = self.snap(raw);
99 if next != self.value {
100 self.value = next;
101 cx.emit(SliderEvent(next));
102 cx.notify();
103 }
104 }
105
106 fn segment_count(&self) -> usize {
107 (((self.max - self.min) / self.step).round() as usize).clamp(1, 200)
108 }
109
110 fn on_key(&mut self, event: &KeyDownEvent, _window: &mut Window, cx: &mut Context<Self>) {
111 match event.keystroke.key.as_str() {
112 "left" | "down" => self.set_value(self.value - self.step, cx),
113 "right" | "up" => self.set_value(self.value + self.step, cx),
114 "home" => self.set_value(self.min, cx),
115 "end" => self.set_value(self.max, cx),
116 _ => return,
117 }
118 cx.stop_propagation();
119 }
120}
121
122impl Render for Slider {
123 fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
124 let t = theme(cx);
125 let accent = t.color(self.color, t.primary_shade()).hsla();
126 let track_color = if t.scheme.is_dark() {
127 t.color(ColorName::Dark, 4)
128 } else {
129 t.color(ColorName::Gray, 2)
130 }
131 .hsla();
132 let knob_bg = t.surface().hsla();
133 let frac = self.fraction();
134
135 let knob = div()
136 .w(px(16.0))
137 .h(px(16.0))
138 .rounded(px(8.0))
139 .bg(knob_bg)
140 .border_2()
141 .border_color(accent);
142
143 let fill = div()
144 .h_full()
145 .w(relative(frac))
146 .rounded(px(3.0))
147 .bg(accent)
148 .flex()
149 .items_center()
150 .justify_end()
151 .child(knob);
152
153 let track = div()
154 .relative()
155 .w_full()
156 .h(px(6.0))
157 .rounded(px(3.0))
158 .bg(track_color)
159 .flex()
160 .items_center()
161 .child(fill);
162
163 let count = self.segment_count();
164 let min = self.min;
165 let span = self.max - self.min;
166 let mut overlay = div()
167 .absolute()
168 .top(px(0.0))
169 .left(px(0.0))
170 .right(px(0.0))
171 .bottom(px(0.0))
172 .flex()
173 .flex_row()
174 .items_center();
175 for i in 0..count {
176 let raw = min + (i as f64) / ((count - 1).max(1) as f64) * span;
177 overlay = overlay.child(
178 div()
179 .id(("guise-slider-seg", i))
180 .flex_grow()
181 .flex_basis(relative(0.0))
182 .h_full()
183 .on_click(cx.listener(move |this, _ev, _window, cx| this.set_value(raw, cx))),
184 );
185 }
186
187 let slider = div()
188 .id("guise-slider")
189 .track_focus(&self.focus)
190 .on_key_down(cx.listener(Self::on_key))
191 .relative()
192 .w_full()
193 .h(px(20.0))
194 .flex()
195 .items_center()
196 .child(track)
197 .child(overlay);
198
199 let value_label = div()
201 .text_size(px(t.font_size(Size::Xs)))
202 .text_color(t.dimmed().hsla())
203 .child(SharedString::from(format!("{}", self.value)));
204
205 let column = div()
206 .flex()
207 .flex_col()
208 .gap(px(4.0))
209 .child(slider)
210 .child(value_label);
211
212 if self.disabled {
213 column.opacity(0.5)
214 } else {
215 column
216 }
217 }
218}