1#![warn(missing_docs)]
24
25use crate::style::resource::StyleResourceExt;
26use crate::style::Style;
27use crate::{
28 border::BorderBuilder,
29 button::{ButtonBuilder, ButtonMessage},
30 core::{
31 num_traits::{clamp, Bounded, NumAssign, NumCast, NumOps},
32 pool::Handle,
33 reflect::{prelude::*, Reflect},
34 type_traits::prelude::*,
35 visitor::prelude::*,
36 },
37 decorator::DecoratorBuilder,
38 grid::{Column, GridBuilder, Row},
39 message::{KeyCode, MessageDirection, MouseButton, UiMessage},
40 text::TextMessage,
41 text_box::{TextBox, TextBoxBuilder, TextCommitMode},
42 utils::{make_arrow, ArrowDirection},
43 widget::{Widget, WidgetBuilder, WidgetMessage},
44 BuildContext, Control, HorizontalAlignment, Thickness, UiNode, UserInterface,
45 VerticalAlignment,
46};
47
48use crate::button::Button;
49use crate::message::MessageData;
50use fyrox_core::variable::InheritableVariable;
51use fyrox_graph::constructor::{ConstructorProvider, GraphNodeConstructor};
52use fyrox_graph::SceneGraph;
53use std::{
54 cmp::Ordering,
55 fmt::{Debug, Display},
56 ops::{Deref, DerefMut},
57 str::FromStr,
58};
59
60pub trait NumericType:
63 NumAssign
64 + FromStr
65 + Clone
66 + Copy
67 + NumOps
68 + PartialOrd
69 + Display
70 + Bounded
71 + Debug
72 + Send
73 + Sync
74 + NumCast
75 + Default
76 + Reflect
77 + Visit
78 + TypeUuidProvider
79 + 'static
80{
81}
82
83impl<T> NumericType for T where
84 T: NumAssign
85 + FromStr
86 + Clone
87 + Copy
88 + NumOps
89 + PartialOrd
90 + Bounded
91 + Display
92 + Debug
93 + Send
94 + Sync
95 + NumCast
96 + Default
97 + Reflect
98 + Visit
99 + TypeUuidProvider
100 + 'static
101{
102}
103
104#[derive(Debug, Clone, PartialEq, Eq)]
107pub enum NumericUpDownMessage<T: NumericType> {
108 Value(T),
111 MinValue(T),
114 MaxValue(T),
117 Step(T),
120 Precision(usize),
123}
124impl<T: NumericType> MessageData for NumericUpDownMessage<T> {}
125
126#[derive(Clone, Debug)]
128pub enum DragContext<T: NumericType> {
129 PreDrag {
131 start_mouse_pos: f32,
133 },
134 Dragging {
136 start_value: T,
138 start_mouse_pos: f32,
140 },
141}
142
143#[derive(Default, Clone, Visit, Reflect, Debug, ComponentProvider)]
240#[reflect(derived_type = "UiNode")]
241pub struct NumericUpDown<T: NumericType> {
242 pub widget: Widget,
244 pub field: InheritableVariable<Handle<TextBox>>,
246 pub increase: InheritableVariable<Handle<Button>>,
248 pub decrease: InheritableVariable<Handle<Button>>,
250 pub value: InheritableVariable<T>,
252 #[visit(skip)]
256 #[reflect(hidden)]
257 formatted_value: T,
258 pub step: InheritableVariable<T>,
260 pub min_value: InheritableVariable<T>,
262 pub max_value: InheritableVariable<T>,
264 pub precision: InheritableVariable<usize>,
266 #[visit(skip)]
268 #[reflect(hidden)]
269 pub drag_context: Option<DragContext<T>>,
270 pub drag_value_scaling: InheritableVariable<f32>,
272}
273
274impl<T: NumericType> ConstructorProvider<UiNode, UserInterface> for NumericUpDown<T> {
275 fn constructor() -> GraphNodeConstructor<UiNode, UserInterface> {
276 GraphNodeConstructor::new::<Self>()
277 .with_variant(
278 format!("Numeric Up Down<{}>", std::any::type_name::<T>()),
279 |ui| {
280 NumericUpDownBuilder::<T>::new(
281 WidgetBuilder::new().with_name("Numeric Up Down"),
282 )
283 .build(&mut ui.build_ctx())
284 .to_base()
285 .into()
286 },
287 )
288 .with_group("Numeric")
289 }
290}
291
292impl<T: NumericType> Deref for NumericUpDown<T> {
293 type Target = Widget;
294
295 fn deref(&self) -> &Self::Target {
296 &self.widget
297 }
298}
299
300impl<T: NumericType> DerefMut for NumericUpDown<T> {
301 fn deref_mut(&mut self) -> &mut Self::Target {
302 &mut self.widget
303 }
304}
305
306impl<T: NumericType> NumericUpDown<T> {
307 fn clamp_value(&self, value: T) -> T {
308 clamp(value, *self.min_value, *self.max_value)
309 }
310
311 fn sync_text_field(&mut self, ui: &UserInterface) {
312 let text = format!("{:.1$}", *self.value, *self.precision);
313 self.formatted_value = text.parse::<T>().unwrap_or(*self.value);
314 let msg = UiMessage::for_widget(
315 *self.field,
316 TextMessage::Text(format!("{:.1$}", *self.value, *self.precision)),
317 );
318 msg.set_handled(true);
319 ui.send_message(msg);
320 }
321
322 fn sync_value_to_bounds_if_needed(&self, ui: &UserInterface) {
323 let clamped = self.clamp_value(*self.value);
324 if *self.value != clamped {
325 ui.send(self.handle, NumericUpDownMessage::Value(clamped));
326 }
327 }
328
329 fn try_parse_value(&mut self, ui: &UserInterface) {
330 if let Ok(field) = ui.try_get(*self.field) {
332 if let Ok(value) = field.text().parse::<T>() {
333 if value != self.formatted_value {
337 self.formatted_value = value;
338 let value = self.clamp_value(value);
339 ui.send(self.handle(), NumericUpDownMessage::Value(value));
340 }
341 } else {
342 self.sync_text_field(ui);
344 }
345 }
346 }
347}
348
349fn saturating_sub<T>(a: T, b: T) -> T
350where
351 T: NumericType,
352{
353 assert!(b >= T::zero());
354
355 if a >= b + T::min_value() {
356 a - b
357 } else {
358 T::min_value()
359 }
360}
361
362fn saturating_add<T>(a: T, b: T) -> T
363where
364 T: NumericType,
365{
366 assert!(b >= T::zero());
367
368 if a < T::max_value() - b {
369 a + b
370 } else {
371 T::max_value()
372 }
373}
374
375fn calculate_value_by_offset<T: NumericType>(
376 start_value: T,
377 offset: i32,
378 step: T,
379 min: T,
380 max: T,
381) -> T {
382 let mut new_value = start_value;
383 match offset.cmp(&0) {
384 Ordering::Less => {
385 for _ in 0..(-offset) {
386 new_value = saturating_sub(new_value, step);
387 }
388 }
389 Ordering::Equal => {}
390 Ordering::Greater => {
391 for _ in 0..offset {
392 new_value = saturating_add(new_value, step);
393 }
394 }
395 }
396 new_value = clamp(new_value, min, max);
397 new_value
398}
399
400impl<T> TypeUuidProvider for NumericUpDown<T>
401where
402 T: NumericType,
403{
404 fn type_uuid() -> Uuid {
405 combine_uuids(
406 uuid!("f852eda4-18e5-4480-83ae-a607ce1c26f7"),
407 T::type_uuid(),
408 )
409 }
410}
411
412impl<T: NumericType> Control for NumericUpDown<T> {
413 fn handle_routed_message(&mut self, ui: &mut UserInterface, message: &mut UiMessage) {
414 self.widget.handle_routed_message(ui, message);
415
416 if let Some(TextMessage::Text(_)) = message.data() {
417 if message.destination() == *self.field
418 && message.direction == MessageDirection::FromWidget
419 && !message.handled()
420 {
421 self.try_parse_value(ui);
422 }
423 } else if let Some(msg) = message.data::<WidgetMessage>() {
424 match msg {
425 WidgetMessage::MouseDown { button, pos, .. } => {
426 if *button == MouseButton::Left
428 && (ui[*self.increase].has_descendant(message.destination(), ui)
429 || ui[*self.decrease].has_descendant(message.destination(), ui))
430 {
431 self.drag_context = Some(DragContext::PreDrag {
432 start_mouse_pos: pos.y,
433 });
434 }
435 }
436 WidgetMessage::MouseMove { pos, .. } => {
437 if let Some(drag_context) = self.drag_context.as_ref() {
438 match drag_context {
439 DragContext::PreDrag { start_mouse_pos } => {
440 if (pos.y - start_mouse_pos).abs() >= 5.0 {
441 self.drag_context = Some(DragContext::Dragging {
442 start_value: *self.value,
443 start_mouse_pos: *start_mouse_pos,
444 });
445 }
446 }
447 DragContext::Dragging {
448 start_value,
449 start_mouse_pos,
450 } => {
451 ui.send(
453 *self.field,
454 TextMessage::Text(format!(
455 "{:.1$}",
456 calculate_value_by_offset(
457 *start_value,
458 ((*start_mouse_pos - pos.y) * *self.drag_value_scaling)
459 as i32,
460 *self.step,
461 *self.min_value,
462 *self.max_value
463 ),
464 *self.precision
465 )),
466 );
467 }
468 }
469 }
470 }
471 WidgetMessage::KeyDown(key_code) => match *key_code {
472 KeyCode::ArrowUp => {
473 ui.post(*self.increase, ButtonMessage::Click);
474 }
475 KeyCode::ArrowDown => {
476 ui.post(*self.decrease, ButtonMessage::Click);
477 }
478 _ => (),
479 },
480 _ => {}
481 }
482 } else if let Some(msg) = message.data_for::<NumericUpDownMessage<T>>(self.handle()) {
483 match msg {
484 NumericUpDownMessage::Value(value) => {
485 #[allow(clippy::eq_op)]
487 fn nan_aware_eq<N: NumericType>(lhs: N, rhs: N) -> bool {
488 let lhs_nan = !(lhs == lhs);
489 let rhs_nan = !(rhs == rhs);
490 if lhs_nan && rhs_nan {
491 true
492 } else if (lhs_nan && !rhs_nan) || (!lhs_nan && rhs_nan) {
493 false
494 } else {
495 lhs == rhs
496 }
497 }
498
499 let clamped = self.clamp_value(*value);
500 if !nan_aware_eq(*self.value, clamped) {
501 self.value.set_value_and_mark_modified(clamped);
502
503 self.sync_text_field(ui);
504
505 let mut msg = UiMessage::from_widget(
506 self.handle,
507 NumericUpDownMessage::Value(*self.value),
508 );
509 msg.set_handled(message.handled());
511 msg.flags = message.flags;
512 msg.delivery_mode = message.delivery_mode;
513 ui.send_message(msg);
514 }
515 }
516 NumericUpDownMessage::MinValue(min_value) => {
517 if (*self.min_value).ne(min_value) {
518 self.min_value.set_value_and_mark_modified(*min_value);
519 ui.send_message(message.reverse());
520 self.sync_value_to_bounds_if_needed(ui);
521 }
522 }
523 NumericUpDownMessage::MaxValue(max_value) => {
524 if (*self.max_value).ne(max_value) {
525 self.max_value.set_value_and_mark_modified(*max_value);
526 ui.send_message(message.reverse());
527 self.sync_value_to_bounds_if_needed(ui);
528 }
529 }
530 NumericUpDownMessage::Step(step) => {
531 if (*self.step).ne(step) {
532 self.step.set_value_and_mark_modified(*step);
533 ui.send_message(message.reverse());
534 self.sync_text_field(ui);
535 }
536 }
537 NumericUpDownMessage::Precision(precision) => {
538 if (*self.precision).ne(precision) {
539 self.precision.set_value_and_mark_modified(*precision);
540 ui.send_message(message.reverse());
541 self.sync_text_field(ui);
542 }
543 }
544 }
545 } else if let Some(ButtonMessage::Click) = message.data::<ButtonMessage>() {
546 if message.destination() == *self.decrease || message.destination() == *self.increase {
547 if let Some(DragContext::Dragging {
548 start_value,
549 start_mouse_pos,
550 }) = self.drag_context.take()
551 {
552 ui.send(
553 self.handle,
554 NumericUpDownMessage::Value(calculate_value_by_offset(
555 start_value,
556 ((start_mouse_pos - ui.cursor_position().y) * *self.drag_value_scaling)
557 as i32,
558 *self.step,
559 *self.min_value,
560 *self.max_value,
561 )),
562 );
563 } else if message.destination() == *self.decrease {
564 let value = self.clamp_value(saturating_sub(*self.value, *self.step));
565 ui.send(self.handle(), NumericUpDownMessage::Value(value));
566 } else if message.destination() == *self.increase {
567 let value = self.clamp_value(saturating_add(*self.value, *self.step));
568 ui.send(self.handle(), NumericUpDownMessage::Value(value));
569 }
570 }
571 }
572 }
573}
574
575pub struct NumericUpDownBuilder<T: NumericType> {
577 widget_builder: WidgetBuilder,
578 value: T,
579 step: T,
580 min_value: T,
581 max_value: T,
582 precision: usize,
583 editable: bool,
584 drag_value_scaling: f32,
585}
586
587fn make_button(
588 ctx: &mut BuildContext,
589 arrow: ArrowDirection,
590 row: usize,
591 editable: bool,
592) -> Handle<Button> {
593 let handle = ButtonBuilder::new(
594 WidgetBuilder::new()
595 .with_enabled(editable)
596 .with_margin(Thickness::right(1.0))
597 .on_row(row),
598 )
599 .with_back(
600 DecoratorBuilder::new(
601 BorderBuilder::new(
602 WidgetBuilder::new().with_foreground(ctx.style.property(Style::BRUSH_LIGHTER)),
603 )
604 .with_corner_radius(2.0f32.into())
605 .with_pad_by_corner_radius(false),
606 )
607 .with_normal_brush(ctx.style.property(Style::BRUSH_PRIMARY))
608 .with_hover_brush(ctx.style.property(Style::BRUSH_LIGHT))
609 .with_pressed_brush(ctx.style.property(Style::BRUSH_BRIGHT_BLUE))
610 .build(ctx),
611 )
612 .with_content(make_arrow(ctx, arrow, 6.0))
613 .build(ctx);
614
615 ctx[handle].accepts_input = false;
617
618 handle
619}
620
621impl<T: NumericType> NumericUpDownBuilder<T> {
622 pub fn new(widget_builder: WidgetBuilder) -> Self {
624 Self {
625 widget_builder,
626 value: T::zero(),
627 step: T::one(),
628 min_value: T::min_value(),
629 max_value: T::max_value(),
630 precision: 3,
631 editable: true,
632 drag_value_scaling: 0.1,
633 }
634 }
635
636 fn set_value(&mut self, value: T) {
637 self.value = clamp(value, self.min_value, self.max_value);
638 }
639
640 pub fn with_min_value(mut self, value: T) -> Self {
642 self.min_value = value;
643 self.set_value(self.value);
644 self
645 }
646
647 pub fn with_max_value(mut self, value: T) -> Self {
649 self.max_value = value;
650 self.set_value(self.value);
651 self
652 }
653
654 pub fn with_value(mut self, value: T) -> Self {
656 self.value = value;
657 self.set_value(value);
658 self
659 }
660
661 pub fn with_step(mut self, step: T) -> Self {
663 assert!(step >= T::zero());
664
665 self.step = step;
666 self
667 }
668
669 pub fn with_precision(mut self, precision: usize) -> Self {
671 self.precision = precision;
672 self
673 }
674
675 pub fn with_editable(mut self, editable: bool) -> Self {
677 self.editable = editable;
678 self
679 }
680
681 pub fn with_drag_value_scaling(mut self, drag_value_scaling: f32) -> Self {
684 self.drag_value_scaling = drag_value_scaling;
685 self
686 }
687
688 pub fn build(self, ctx: &mut BuildContext) -> Handle<NumericUpDown<T>> {
690 let increase;
691 let decrease;
692 let field;
693 let back = BorderBuilder::new(
694 WidgetBuilder::new()
695 .with_background(ctx.style.property(Style::BRUSH_DARK))
696 .with_foreground(ctx.style.property(Style::BRUSH_LIGHT)),
697 )
698 .with_corner_radius(4.0f32.into())
699 .with_pad_by_corner_radius(false)
700 .with_stroke_thickness(Thickness::uniform(1.0).into())
701 .build(ctx);
702
703 let text = format!("{:.1$}", self.value, self.precision);
704 let formatted_value = text.parse::<T>().unwrap_or(self.value);
705 let grid = GridBuilder::new(
706 WidgetBuilder::new()
707 .with_child({
708 field = TextBoxBuilder::new(
709 WidgetBuilder::new()
710 .on_row(0)
711 .on_column(0)
712 .with_margin(Thickness::left(2.0)),
713 )
714 .with_vertical_text_alignment(VerticalAlignment::Center)
715 .with_horizontal_text_alignment(HorizontalAlignment::Left)
716 .with_text_commit_mode(TextCommitMode::Changed)
717 .with_text(text)
718 .with_editable(self.editable)
719 .build(ctx);
720 field
721 })
722 .with_child(
723 GridBuilder::new(
724 WidgetBuilder::new()
725 .on_column(1)
726 .with_child({
727 increase = make_button(ctx, ArrowDirection::Top, 0, self.editable);
728 increase
729 })
730 .with_child({
731 decrease =
732 make_button(ctx, ArrowDirection::Bottom, 1, self.editable);
733 decrease
734 }),
735 )
736 .add_column(Column::auto())
737 .add_row(Row::stretch())
738 .add_row(Row::stretch())
739 .build(ctx),
740 ),
741 )
742 .add_row(Row::stretch())
743 .add_column(Column::stretch())
744 .add_column(Column::auto())
745 .build(ctx);
746
747 ctx.link(grid, back);
748
749 let node = NumericUpDown {
750 widget: self.widget_builder.with_child(back).build(ctx),
751 increase: increase.into(),
752 decrease: decrease.into(),
753 field: field.into(),
754 value: self.value.into(),
755 formatted_value,
756 step: self.step.into(),
757 min_value: self.min_value.into(),
758 max_value: self.max_value.into(),
759 precision: self.precision.into(),
760 drag_context: None,
761 drag_value_scaling: self.drag_value_scaling.into(),
762 };
763
764 ctx.add(node)
765 }
766}
767
768#[cfg(test)]
769mod test {
770
771 use crate::numeric::NumericUpDownBuilder;
772 use crate::{
773 numeric::{saturating_add, saturating_sub},
774 test::test_widget_deletion,
775 widget::WidgetBuilder,
776 };
777
778 #[test]
779 fn test_saturating_add() {
780 assert_eq!(saturating_add(0, 1), 1);
782 assert_eq!(saturating_add(1, 0), 1);
783 assert_eq!(saturating_add(0, 0), 0);
784 assert_eq!(saturating_add(1, 1), 2);
785 assert_eq!(saturating_add(i32::MAX, 1), i32::MAX);
786 assert_eq!(saturating_add(i32::MIN, 1), i32::MIN + 1);
787
788 assert_eq!(saturating_add(0.0, 1.0), 1.0);
790 assert_eq!(saturating_add(1.0, 0.0), 1.0);
791 assert_eq!(saturating_add(f32::MAX, 1.0), f32::MAX);
792 assert_eq!(saturating_add(f32::MIN, 1.0), f32::MIN + 1.0);
793 }
794
795 #[test]
796 fn test_saturating_sub() {
797 assert_eq!(saturating_sub(0, 0), 0);
799 assert_eq!(saturating_sub(0, 1), -1);
800 assert_eq!(saturating_sub(1, 1), 0);
801 assert_eq!(saturating_sub(1, 0), 1);
802 assert_eq!(saturating_sub(10, 10), 0);
803 assert_eq!(saturating_sub(i32::MIN, 1), i32::MIN);
804 assert_eq!(saturating_sub(i32::MAX, 1), i32::MAX - 1);
805
806 assert_eq!(saturating_sub(0u32, 0u32), 0u32);
808 assert_eq!(saturating_sub(0u32, 1u32), 0u32);
809 assert_eq!(saturating_sub(1u32, 1u32), 0u32);
810 assert_eq!(saturating_sub(1u32, 0u32), 1u32);
811 assert_eq!(saturating_sub(10u32, 10u32), 0u32);
812 assert_eq!(saturating_sub(u32::MIN, 1u32), u32::MIN);
813 assert_eq!(saturating_sub(u32::MAX, 1u32), u32::MAX - 1);
814
815 assert_eq!(saturating_sub(0.0, 1.0), -1.0);
817 assert_eq!(saturating_sub(1.0, 0.0), 1.0);
818 assert_eq!(saturating_sub(1.0, 1.0), 0.0);
819 assert_eq!(saturating_sub(f32::MIN, 1.0), f32::MIN);
820 assert_eq!(saturating_sub(f32::MAX, 1.0), f32::MAX - 1.0);
821 }
822
823 #[test]
824 fn test_deletion() {
825 test_widget_deletion(|ctx| {
826 NumericUpDownBuilder::<f32>::new(WidgetBuilder::new()).build(ctx)
827 });
828 }
829}