1use crate::animation::{DragMode, VelocityDrag, VelocityDragConfig};
11use crate::ext::ArmasContextExt;
12use egui::{pos2, vec2, Color32, Rect, Sense, Stroke, Ui};
13
14const TRACK_HEIGHT: f32 = 6.0; const THUMB_RADIUS: f32 = 8.0; #[derive(Clone)]
20struct SliderDragState {
21 drag: VelocityDrag,
22 drag_start_value: f32,
23}
24
25impl Default for SliderDragState {
26 fn default() -> Self {
27 Self {
28 drag: VelocityDrag::new(VelocityDragConfig::new().sensitivity(1.0)),
29 drag_start_value: 0.0,
30 }
31 }
32}
33
34pub struct Slider {
54 id: Option<egui::Id>,
55 min: f32,
56 max: f32,
57 width: f32,
58 height: f32,
59 show_value: bool,
60 label: Option<String>,
61 suffix: Option<String>,
62 step: Option<f32>,
63 default_value: Option<f32>,
64 velocity_mode: bool,
65 sensitivity: f64,
66}
67
68impl Slider {
69 #[must_use]
71 pub const fn new(min: f32, max: f32) -> Self {
72 Self {
73 id: None,
74 min,
75 max,
76 width: 200.0,
77 height: 20.0,
78 show_value: true,
79 label: None,
80 suffix: None,
81 step: None,
82 default_value: None,
83 velocity_mode: false,
84 sensitivity: 1.0,
85 }
86 }
87
88 #[must_use]
90 pub fn id(mut self, id: impl Into<egui::Id>) -> Self {
91 self.id = Some(id.into());
92 self
93 }
94
95 #[must_use]
97 pub const fn width(mut self, width: f32) -> Self {
98 self.width = width;
99 self
100 }
101
102 #[must_use]
104 pub const fn height(mut self, height: f32) -> Self {
105 self.height = height;
106 self
107 }
108
109 #[must_use]
111 pub const fn show_value(mut self, show: bool) -> Self {
112 self.show_value = show;
113 self
114 }
115
116 #[must_use]
118 pub fn label(mut self, label: impl Into<String>) -> Self {
119 self.label = Some(label.into());
120 self
121 }
122
123 #[must_use]
125 pub fn suffix(mut self, suffix: impl Into<String>) -> Self {
126 self.suffix = Some(suffix.into());
127 self
128 }
129
130 #[must_use]
132 pub const fn step(mut self, step: f32) -> Self {
133 self.step = Some(step);
134 self
135 }
136
137 #[must_use]
139 pub const fn default_value(mut self, value: f32) -> Self {
140 self.default_value = Some(value);
141 self
142 }
143
144 #[must_use]
150 pub const fn velocity_mode(mut self, enabled: bool) -> Self {
151 self.velocity_mode = enabled;
152 self
153 }
154
155 #[must_use]
159 pub const fn sensitivity(mut self, sensitivity: f64) -> Self {
160 self.sensitivity = sensitivity;
161 self
162 }
163
164 pub fn show(self, ui: &mut Ui, value: &mut f32) -> SliderResponse {
166 let theme = ui.ctx().armas_theme();
167 let mut changed = false;
168
169 let slider_id = self.id.unwrap_or_else(|| ui.make_persistent_id("slider"));
171 let drag_state_id = slider_id.with("drag_state");
172
173 if let Some(id) = self.id {
175 let state_id = id.with("slider_state");
176 let stored_value: f32 = ui
177 .ctx()
178 .data_mut(|d| d.get_temp(state_id).unwrap_or(*value));
179 *value = stored_value;
180 }
181
182 *value = value.clamp(self.min, self.max);
184
185 ui.vertical(|ui| {
186 ui.spacing_mut().item_spacing.y = 4.0;
187 if let Some(label) = &self.label {
189 ui.horizontal(|ui| {
190 ui.spacing_mut().item_spacing.x = 8.0;
191 ui.label(label);
192
193 if self.show_value {
194 ui.allocate_space(ui.available_size());
195
196 let value_text = self.suffix.as_ref().map_or_else(
197 || format!("{value:.1}"),
198 |suffix| format!("{value:.1}{suffix}"),
199 );
200 ui.label(value_text);
201 }
202 });
203 }
204
205 let slider_width = self.width;
207 let (rect, response) =
208 ui.allocate_exact_size(vec2(slider_width, self.height), Sense::click_and_drag());
209
210 if response.double_clicked() {
212 if let Some(default) = self.default_value {
213 if (*value - default).abs() > 0.001 {
214 *value = default;
215 changed = true;
216 }
217 }
218 }
219 else if response.drag_started() {
221 let mut drag_state = SliderDragState {
222 drag: VelocityDrag::new(
223 VelocityDragConfig::new().sensitivity(self.sensitivity),
224 ),
225 drag_start_value: *value,
226 };
227
228 if let Some(pos) = response.interact_pointer_pos() {
229 let use_velocity =
230 self.velocity_mode && ui.input(|i| i.modifiers.command || i.modifiers.ctrl);
231 drag_state
232 .drag
233 .begin(f64::from(*value), f64::from(pos.x), use_velocity);
234 }
235
236 ui.ctx()
237 .data_mut(|d| d.insert_temp(drag_state_id, drag_state));
238 } else if response.dragged() {
239 if let Some(pos) = response.interact_pointer_pos() {
240 let mut drag_state: SliderDragState = ui
241 .ctx()
242 .data_mut(|d| d.get_temp(drag_state_id).unwrap_or_default());
243
244 let range = f64::from(self.max - self.min);
245
246 if drag_state.drag.mode() == DragMode::Velocity {
247 let delta = drag_state.drag.update_tracked(
249 f64::from(pos.x),
250 range,
251 f64::from(rect.width()),
252 );
253 let mut new_value = drag_state.drag_start_value + delta as f32;
254
255 if let Some(step) = self.step {
257 new_value = (new_value / step).round() * step;
258 }
259
260 if (new_value - *value).abs() > 0.001 {
261 *value = new_value.clamp(self.min, self.max);
262 changed = true;
263 }
264 } else {
265 let t = ((pos.x - rect.left()) / rect.width()).clamp(0.0, 1.0);
267 let mut new_value = self.min + t * (self.max - self.min);
268
269 if let Some(step) = self.step {
271 new_value = (new_value / step).round() * step;
272 }
273
274 if (new_value - *value).abs() > 0.001 {
275 *value = new_value.clamp(self.min, self.max);
276 changed = true;
277 }
278 }
279
280 ui.ctx()
281 .data_mut(|d| d.insert_temp(drag_state_id, drag_state));
282 }
283 } else if response.drag_stopped() {
284 ui.ctx().data_mut(|d| {
285 let mut drag_state: SliderDragState =
286 d.get_temp(drag_state_id).unwrap_or_default();
287 drag_state.drag.end();
288 d.insert_temp(drag_state_id, drag_state);
289 });
290 }
291 else if response.clicked() {
293 if let Some(pos) = response.interact_pointer_pos() {
294 let t = ((pos.x - rect.left()) / rect.width()).clamp(0.0, 1.0);
295 let mut new_value = self.min + t * (self.max - self.min);
296
297 if let Some(step) = self.step {
299 new_value = (new_value / step).round() * step;
300 }
301
302 if (new_value - *value).abs() > 0.001 {
303 *value = new_value.clamp(self.min, self.max);
304 changed = true;
305 }
306 }
307 }
308
309 if ui.is_rect_visible(rect) {
310 let painter = ui.painter();
311
312 let track_rect =
314 Rect::from_center_size(rect.center(), vec2(rect.width(), TRACK_HEIGHT));
315
316 painter.rect_filled(track_rect, TRACK_HEIGHT / 2.0, theme.muted());
317
318 let t = (*value - self.min) / (self.max - self.min);
320 let fill_width = track_rect.width() * t;
321 let fill_rect = Rect::from_min_size(track_rect.min, vec2(fill_width, TRACK_HEIGHT));
322
323 painter.rect_filled(fill_rect, TRACK_HEIGHT / 2.0, theme.primary());
324
325 let handle_x = track_rect.left() + fill_width;
327 let handle_center = pos2(handle_x, track_rect.center().y);
328
329 if response.hovered() || response.dragged() {
331 let ring_color = theme.ring().gamma_multiply(0.5);
332 painter.circle_filled(handle_center, THUMB_RADIUS + 4.0, ring_color);
333 }
334
335 painter.circle_filled(
337 handle_center + vec2(0.0, 1.0),
338 THUMB_RADIUS,
339 Color32::from_black_alpha(40),
340 );
341
342 let handle_color = if response.dragged() {
344 theme.primary()
345 } else {
346 theme.foreground()
347 };
348
349 painter.circle_filled(handle_center, THUMB_RADIUS, handle_color);
350
351 painter.circle_stroke(
353 handle_center,
354 THUMB_RADIUS,
355 Stroke::new(1.0, theme.primary()),
356 );
357 }
358 });
359
360 if let Some(id) = self.id {
362 let state_id = id.with("slider_state");
363 ui.ctx().data_mut(|d| {
364 d.insert_temp(state_id, *value);
365 });
366 }
367
368 let response = ui.interact(ui.min_rect(), slider_id.with("response"), Sense::hover());
369
370 SliderResponse {
371 response,
372 value: *value,
373 changed,
374 }
375 }
376}
377
378pub struct SliderResponse {
380 pub response: egui::Response,
382 pub value: f32,
384 pub changed: bool,
386}