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 avail = ui.available_width();
207 let slider_width = if avail.is_finite() && avail > 0.0 {
208 avail
209 } else {
210 self.width
211 };
212 let (rect, response) =
213 ui.allocate_exact_size(vec2(slider_width, self.height), Sense::click_and_drag());
214
215 if response.double_clicked() {
217 if let Some(default) = self.default_value {
218 if (*value - default).abs() > 0.001 {
219 *value = default;
220 changed = true;
221 }
222 }
223 }
224 else if response.drag_started() {
226 let mut drag_state = SliderDragState {
227 drag: VelocityDrag::new(
228 VelocityDragConfig::new().sensitivity(self.sensitivity),
229 ),
230 drag_start_value: *value,
231 };
232
233 if let Some(pos) = response.interact_pointer_pos() {
234 let use_velocity =
235 self.velocity_mode && ui.input(|i| i.modifiers.command || i.modifiers.ctrl);
236 drag_state
237 .drag
238 .begin(f64::from(*value), f64::from(pos.x), use_velocity);
239 }
240
241 ui.ctx()
242 .data_mut(|d| d.insert_temp(drag_state_id, drag_state));
243 } else if response.dragged() {
244 if let Some(pos) = response.interact_pointer_pos() {
245 let mut drag_state: SliderDragState = ui
246 .ctx()
247 .data_mut(|d| d.get_temp(drag_state_id).unwrap_or_default());
248
249 let range = f64::from(self.max - self.min);
250
251 if drag_state.drag.mode() == DragMode::Velocity {
252 let delta = drag_state.drag.update_tracked(
254 f64::from(pos.x),
255 range,
256 f64::from(rect.width()),
257 );
258 let mut new_value = drag_state.drag_start_value + delta as f32;
259
260 if let Some(step) = self.step {
262 new_value = (new_value / step).round() * step;
263 }
264
265 if (new_value - *value).abs() > 0.001 {
266 *value = new_value.clamp(self.min, self.max);
267 changed = true;
268 }
269 } else {
270 let t = ((pos.x - rect.left()) / rect.width()).clamp(0.0, 1.0);
272 let mut new_value = self.min + t * (self.max - self.min);
273
274 if let Some(step) = self.step {
276 new_value = (new_value / step).round() * step;
277 }
278
279 if (new_value - *value).abs() > 0.001 {
280 *value = new_value.clamp(self.min, self.max);
281 changed = true;
282 }
283 }
284
285 ui.ctx()
286 .data_mut(|d| d.insert_temp(drag_state_id, drag_state));
287 }
288 } else if response.drag_stopped() {
289 ui.ctx().data_mut(|d| {
290 let mut drag_state: SliderDragState =
291 d.get_temp(drag_state_id).unwrap_or_default();
292 drag_state.drag.end();
293 d.insert_temp(drag_state_id, drag_state);
294 });
295 }
296 else if response.clicked() {
298 if let Some(pos) = response.interact_pointer_pos() {
299 let t = ((pos.x - rect.left()) / rect.width()).clamp(0.0, 1.0);
300 let mut new_value = self.min + t * (self.max - self.min);
301
302 if let Some(step) = self.step {
304 new_value = (new_value / step).round() * step;
305 }
306
307 if (new_value - *value).abs() > 0.001 {
308 *value = new_value.clamp(self.min, self.max);
309 changed = true;
310 }
311 }
312 }
313
314 if ui.is_rect_visible(rect) {
315 let painter = ui.painter();
316
317 let track_rect =
319 Rect::from_center_size(rect.center(), vec2(rect.width(), TRACK_HEIGHT));
320
321 painter.rect_filled(track_rect, TRACK_HEIGHT / 2.0, theme.muted());
322
323 let t = (*value - self.min) / (self.max - self.min);
325 let fill_width = track_rect.width() * t;
326 let fill_rect = Rect::from_min_size(track_rect.min, vec2(fill_width, TRACK_HEIGHT));
327
328 painter.rect_filled(fill_rect, TRACK_HEIGHT / 2.0, theme.primary());
329
330 let handle_x = track_rect.left() + fill_width;
332 let handle_center = pos2(handle_x, track_rect.center().y);
333
334 if response.hovered() || response.dragged() {
336 let ring_color = theme.ring().gamma_multiply(0.5);
337 painter.circle_filled(handle_center, THUMB_RADIUS + 4.0, ring_color);
338 }
339
340 painter.circle_filled(
342 handle_center + vec2(0.0, 1.0),
343 THUMB_RADIUS,
344 Color32::from_black_alpha(40),
345 );
346
347 let handle_color = if response.dragged() {
349 theme.primary()
350 } else {
351 theme.foreground()
352 };
353
354 painter.circle_filled(handle_center, THUMB_RADIUS, handle_color);
355
356 painter.circle_stroke(
358 handle_center,
359 THUMB_RADIUS,
360 Stroke::new(1.0, theme.primary()),
361 );
362 }
363 });
364
365 if let Some(id) = self.id {
367 let state_id = id.with("slider_state");
368 ui.ctx().data_mut(|d| {
369 d.insert_temp(state_id, *value);
370 });
371 }
372
373 let response = ui.interact(ui.min_rect(), slider_id.with("response"), Sense::hover());
374
375 SliderResponse {
376 response,
377 value: *value,
378 changed,
379 }
380 }
381}
382
383pub struct SliderResponse {
385 pub response: egui::Response,
387 pub value: f32,
389 pub changed: bool,
391}