1use std::cell::Cell;
4use std::rc::Rc;
5use std::sync::Arc;
6
7use crate::draw_ctx::DrawCtx;
8use crate::event::{Event, EventResult, Key, MouseButton};
9use crate::geometry::{Rect, Size};
10use crate::layout_props::{HAnchor, Insets, VAnchor, WidgetBase};
11use crate::text::Font;
12use crate::widget::{paint_subtree, Widget};
13use crate::widgets::label::{Label, LabelAlign};
14
15const TRACK_H: f64 = 4.0;
16const THUMB_R: f64 = 7.0;
17const WIDGET_H: f64 = 22.0;
21const VALUE_W: f64 = 44.0;
25const VALUE_GAP: f64 = 6.0;
27
28#[cfg_attr(feature = "reflect", derive(bevy_reflect::Reflect))]
43#[derive(Clone, Debug)]
44pub struct SliderProps {
45 pub value: f64,
46 pub min: f64,
47 pub max: f64,
48 pub step: f64,
49 pub show_value: bool,
50 pub decimals: Option<usize>,
53 pub font_size: f64,
54}
55
56impl Default for SliderProps {
57 fn default() -> Self {
58 Self {
59 value: 0.0,
60 min: 0.0,
61 max: 1.0,
62 step: 0.01,
63 show_value: true,
64 decimals: None,
65 font_size: 12.0,
66 }
67 }
68}
69
70pub struct Slider {
72 bounds: Rect,
73 children: Vec<Box<dyn Widget>>, base: WidgetBase,
75 pub props: SliderProps,
77 dragging: bool,
78 focused: bool,
79 hovered: bool,
80 on_change: Option<Box<dyn FnMut(f64)>>,
81 value_cell: Option<Rc<Cell<f64>>>,
87 value_label: Label,
91 last_value_text: String,
94}
95
96impl Slider {
97 pub fn new(value: f64, min: f64, max: f64, font: Arc<Font>) -> Self {
98 let v = value.clamp(min, max);
99 let font_size = 12.0;
100 let value_label = Label::new("", Arc::clone(&font))
101 .with_font_size(font_size)
102 .with_align(LabelAlign::Right);
103 Self {
104 bounds: Rect::default(),
105 children: Vec::new(),
106 base: WidgetBase::new(),
107 props: SliderProps {
108 value: v,
109 min,
110 max,
111 step: (max - min) / 100.0,
112 show_value: true,
113 decimals: None,
114 font_size,
115 },
116 dragging: false,
117 focused: false,
118 hovered: false,
119 on_change: None,
120 value_cell: None,
121 value_label,
122 last_value_text: String::new(),
123 }
124 }
125
126 pub fn with_step(mut self, step: f64) -> Self {
127 self.props.step = step;
128 self
129 }
130
131 pub fn with_value_cell(mut self, cell: Rc<Cell<f64>>) -> Self {
138 self.props.value = cell.get().clamp(self.props.min, self.props.max);
139 self.value_cell = Some(cell);
140 self
141 }
142 pub fn with_show_value(mut self, show: bool) -> Self {
143 self.props.show_value = show;
144 self
145 }
146
147 pub fn with_decimals(mut self, decimals: usize) -> Self {
150 self.props.decimals = Some(decimals);
151 self
152 }
153
154 pub fn with_margin(mut self, m: Insets) -> Self {
155 self.base.margin = m;
156 self
157 }
158 pub fn with_h_anchor(mut self, h: HAnchor) -> Self {
159 self.base.h_anchor = h;
160 self
161 }
162 pub fn with_v_anchor(mut self, v: VAnchor) -> Self {
163 self.base.v_anchor = v;
164 self
165 }
166 pub fn with_min_size(mut self, s: Size) -> Self {
167 self.base.min_size = s;
168 self
169 }
170 pub fn with_max_size(mut self, s: Size) -> Self {
171 self.base.max_size = s;
172 self
173 }
174
175 pub fn on_change(mut self, cb: impl FnMut(f64) + 'static) -> Self {
176 self.on_change = Some(Box::new(cb));
177 self
178 }
179
180 pub fn value(&self) -> f64 {
181 self.props.value
182 }
183
184 pub fn set_value(&mut self, v: f64) {
185 self.props.value = v.clamp(self.props.min, self.props.max);
186 if let Some(cell) = &self.value_cell {
187 cell.set(self.props.value);
188 }
189 }
190
191 fn fire(&mut self) {
192 let v = self.props.value;
193 if let Some(cell) = &self.value_cell {
194 cell.set(v);
195 }
196 if let Some(cb) = self.on_change.as_mut() {
197 cb(v);
198 }
199 }
200
201 fn track_right(&self) -> f64 {
205 let reserved = if self.props.show_value {
206 VALUE_W + VALUE_GAP
207 } else {
208 0.0
209 };
210 (self.bounds.width - reserved - THUMB_R).max(THUMB_R + 1.0)
211 }
212
213 fn thumb_x(&self) -> f64 {
215 let track_left = THUMB_R;
216 let track_right = self.track_right();
217 let t = if self.props.max > self.props.min {
218 (self.props.value - self.props.min) / (self.props.max - self.props.min)
219 } else {
220 0.0
221 };
222 track_left + t * (track_right - track_left)
223 }
224
225 fn value_from_x(&self, x: f64) -> f64 {
226 let track_left = THUMB_R;
227 let track_right = self.track_right();
228 let t = ((x - track_left) / (track_right - track_left)).clamp(0.0, 1.0);
229 let raw = self.props.min + t * (self.props.max - self.props.min);
230 let snapped = (raw / self.props.step).round() * self.props.step;
232 snapped.clamp(self.props.min, self.props.max)
233 }
234
235 fn format_value(&self) -> String {
238 if let Some(d) = self.props.decimals {
239 return format!("{:.*}", d, self.props.value);
240 }
241 if self.props.step >= 1.0 {
242 format!("{:.0}", self.props.value)
243 } else if self.props.step >= 0.1 {
244 format!("{:.1}", self.props.value)
245 } else if self.props.step >= 0.01 {
246 format!("{:.2}", self.props.value)
247 } else {
248 format!("{:.3}", self.props.value)
249 }
250 }
251}
252
253impl Widget for Slider {
254 fn type_name(&self) -> &'static str {
255 "Slider"
256 }
257 fn bounds(&self) -> Rect {
258 self.bounds
259 }
260 fn set_bounds(&mut self, b: Rect) {
261 self.bounds = b;
262 }
263 fn children(&self) -> &[Box<dyn Widget>] {
264 &self.children
265 }
266 fn children_mut(&mut self) -> &mut Vec<Box<dyn Widget>> {
267 &mut self.children
268 }
269
270 #[cfg(feature = "reflect")]
271 fn as_reflect(&self) -> Option<&dyn bevy_reflect::Reflect> {
272 Some(&self.props)
273 }
274 #[cfg(feature = "reflect")]
275 fn as_reflect_mut(&mut self) -> Option<&mut dyn bevy_reflect::Reflect> {
276 Some(&mut self.props)
277 }
278
279 fn is_focusable(&self) -> bool {
280 true
281 }
282
283 fn margin(&self) -> Insets {
284 self.base.margin
285 }
286 fn widget_base(&self) -> Option<&WidgetBase> {
287 Some(&self.base)
288 }
289 fn widget_base_mut(&mut self) -> Option<&mut WidgetBase> {
290 Some(&mut self.base)
291 }
292 fn h_anchor(&self) -> HAnchor {
293 self.base.h_anchor
294 }
295 fn v_anchor(&self) -> VAnchor {
296 self.base.v_anchor
297 }
298 fn min_size(&self) -> Size {
299 self.base.min_size
300 }
301 fn max_size(&self) -> Size {
302 self.base.max_size
303 }
304
305 fn layout(&mut self, available: Size) -> Size {
306 if !self.dragging {
311 if let Some(cell) = &self.value_cell {
312 self.props.value = cell.get().clamp(self.props.min, self.props.max);
313 }
314 }
315
316 if self.props.show_value {
321 let new_text = self.format_value();
322 if new_text != self.last_value_text {
323 self.value_label.set_text(new_text.clone());
324 self.last_value_text = new_text;
325 }
326 let lh = self.props.font_size * 1.5;
329 let _ = self.value_label.layout(Size::new(VALUE_W, lh));
330 self.value_label
331 .set_bounds(Rect::new(0.0, 0.0, VALUE_W, lh));
332 }
333
334 Size::new(available.width, WIDGET_H)
335 }
336
337 fn paint(&mut self, ctx: &mut dyn DrawCtx) {
338 let v = ctx.visuals();
339 let h = self.bounds.height;
340 let cy = h * 0.5;
341
342 let track_right = self.track_right();
343 let track_w = (track_right - THUMB_R).max(0.0);
344
345 ctx.set_fill_color(v.track_bg);
347 ctx.begin_path();
348 ctx.rounded_rect(THUMB_R, cy - TRACK_H * 0.5, track_w, TRACK_H, TRACK_H * 0.5);
349 ctx.fill();
350
351 let tx = self.thumb_x();
353 if tx > THUMB_R {
354 ctx.set_fill_color(v.accent);
355 ctx.begin_path();
356 ctx.rounded_rect(
357 THUMB_R,
358 cy - TRACK_H * 0.5,
359 tx - THUMB_R,
360 TRACK_H,
361 TRACK_H * 0.5,
362 );
363 ctx.fill();
364 }
365
366 if self.focused {
368 ctx.set_stroke_color(v.accent_focus);
369 ctx.set_line_width(2.0);
370 ctx.begin_path();
371 ctx.circle(tx, cy, THUMB_R + 3.0);
372 ctx.stroke();
373 }
374
375 let thumb_color = if self.dragging || self.focused {
377 v.accent_pressed
378 } else if self.hovered {
379 v.accent_hovered
380 } else {
381 v.accent
382 };
383 ctx.set_fill_color(thumb_color);
384 ctx.begin_path();
385 ctx.circle(tx, cy, THUMB_R);
386 ctx.fill();
387
388 ctx.set_fill_color(v.widget_bg);
389 ctx.begin_path();
390 ctx.circle(tx, cy, THUMB_R - 2.5);
391 ctx.fill();
392
393 if self.props.show_value {
398 self.value_label.set_color(v.text_color);
399 let lb = self.value_label.bounds();
400 let strip_left = track_right + VALUE_GAP;
401 let ly = cy - lb.height * 0.5;
402 self.value_label
403 .set_bounds(Rect::new(strip_left, ly, lb.width, lb.height));
404 ctx.save();
405 ctx.translate(strip_left, ly);
406 paint_subtree(&mut self.value_label, ctx);
407 ctx.restore();
408 }
409 }
410
411 fn on_event(&mut self, event: &Event) -> EventResult {
412 match event {
413 Event::MouseMove { pos } => {
414 let was = self.hovered;
415 self.hovered = self.hit_test(*pos);
416 if self.dragging {
417 self.props.value = self.value_from_x(pos.x);
418 self.fire();
419 crate::animation::request_draw();
420 return EventResult::Consumed;
421 }
422 if was != self.hovered {
423 crate::animation::request_draw();
424 return EventResult::Consumed;
425 }
426 EventResult::Ignored
427 }
428 Event::MouseDown {
429 button: MouseButton::Left,
430 pos,
431 ..
432 } => {
433 self.dragging = true;
434 self.props.value = self.value_from_x(pos.x);
435 self.fire();
436 crate::animation::request_draw();
437 EventResult::Consumed
438 }
439 Event::MouseUp {
440 button: MouseButton::Left,
441 ..
442 } => {
443 let was = self.dragging;
444 self.dragging = false;
445 if was {
446 crate::animation::request_draw();
447 }
448 EventResult::Consumed
449 }
450 Event::KeyDown { key, .. } => {
451 let changed = match key {
452 Key::ArrowLeft => {
453 self.props.value =
454 (self.props.value - self.props.step).clamp(self.props.min, self.props.max);
455 true
456 }
457 Key::ArrowRight => {
458 self.props.value =
459 (self.props.value + self.props.step).clamp(self.props.min, self.props.max);
460 true
461 }
462 Key::ArrowDown => {
463 self.props.value = (self.props.value - self.props.step * 10.0)
464 .clamp(self.props.min, self.props.max);
465 true
466 }
467 Key::ArrowUp => {
468 self.props.value = (self.props.value + self.props.step * 10.0)
469 .clamp(self.props.min, self.props.max);
470 true
471 }
472 _ => false,
473 };
474 if changed {
475 self.fire();
476 crate::animation::request_draw();
477 EventResult::Consumed
478 } else {
479 EventResult::Ignored
480 }
481 }
482 Event::FocusGained => {
483 let was = self.focused;
484 self.focused = true;
485 if !was {
486 crate::animation::request_draw();
487 EventResult::Consumed
488 } else {
489 EventResult::Ignored
490 }
491 }
492 Event::FocusLost => {
493 let was_focused = self.focused;
494 let was_dragging = self.dragging;
495 self.focused = false;
496 self.dragging = false;
497 if was_focused || was_dragging {
498 crate::animation::request_draw();
499 EventResult::Consumed
500 } else {
501 EventResult::Ignored
502 }
503 }
504 _ => EventResult::Ignored,
505 }
506 }
507}