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 define_constructor,
39 grid::{Column, GridBuilder, Row},
40 message::{KeyCode, MessageDirection, MouseButton, UiMessage},
41 text::TextMessage,
42 text_box::{TextBox, TextBoxBuilder, TextCommitMode},
43 utils::{make_arrow, ArrowDirection},
44 widget::{Widget, WidgetBuilder, WidgetMessage},
45 BuildContext, Control, HorizontalAlignment, Thickness, UiNode, UserInterface,
46 VerticalAlignment,
47};
48use fyrox_core::variable::InheritableVariable;
49use fyrox_graph::constructor::{ConstructorProvider, GraphNodeConstructor};
50use fyrox_graph::BaseSceneGraph;
51use std::{
52 cmp::Ordering,
53 fmt::{Debug, Display},
54 ops::{Deref, DerefMut},
55 str::FromStr,
56};
57
58pub trait NumericType:
61 NumAssign
62 + FromStr
63 + Clone
64 + Copy
65 + NumOps
66 + PartialOrd
67 + Display
68 + Bounded
69 + Debug
70 + Send
71 + Sync
72 + NumCast
73 + Default
74 + Reflect
75 + Visit
76 + TypeUuidProvider
77 + 'static
78{
79}
80
81impl<T> NumericType for T where
82 T: NumAssign
83 + FromStr
84 + Clone
85 + Copy
86 + NumOps
87 + PartialOrd
88 + Bounded
89 + Display
90 + Debug
91 + Send
92 + Sync
93 + NumCast
94 + Default
95 + Reflect
96 + Visit
97 + TypeUuidProvider
98 + 'static
99{
100}
101
102#[derive(Debug, Clone, PartialEq, Eq)]
105pub enum NumericUpDownMessage<T: NumericType> {
106 Value(T),
109 MinValue(T),
112 MaxValue(T),
115 Step(T),
118 Precision(usize),
121}
122
123impl<T: NumericType> NumericUpDownMessage<T> {
124 define_constructor!(
125 NumericUpDownMessage:Value => fn value(T), layout: false
127 );
128 define_constructor!(
129 NumericUpDownMessage:MinValue => fn min_value(T), layout: false
131 );
132 define_constructor!(
133 NumericUpDownMessage:MaxValue => fn max_value(T), layout: false
135 );
136 define_constructor!(
137 NumericUpDownMessage:Step => fn step(T), layout: false
139 );
140
141 pub fn precision(
143 destination: Handle<UiNode>,
144 direction: MessageDirection,
145 precision: usize,
146 ) -> UiMessage {
147 UiMessage {
148 handled: Default::default(),
149 data: Box::new(precision),
150 destination,
151 direction,
152 routing_strategy: Default::default(),
153 perform_layout: Default::default(),
154 flags: 0,
155 }
156 }
157}
158
159#[derive(Clone, Debug)]
161pub enum DragContext<T: NumericType> {
162 PreDrag {
164 start_mouse_pos: f32,
166 },
167 Dragging {
169 start_value: T,
171 start_mouse_pos: f32,
173 },
174}
175
176#[derive(Default, Clone, Visit, Reflect, Debug, ComponentProvider)]
265pub struct NumericUpDown<T: NumericType> {
266 pub widget: Widget,
268 pub field: InheritableVariable<Handle<UiNode>>,
270 pub increase: InheritableVariable<Handle<UiNode>>,
272 pub decrease: InheritableVariable<Handle<UiNode>>,
274 pub value: InheritableVariable<T>,
276 #[visit(skip)]
280 #[reflect(hidden)]
281 formatted_value: T,
282 pub step: InheritableVariable<T>,
284 pub min_value: InheritableVariable<T>,
286 pub max_value: InheritableVariable<T>,
288 pub precision: InheritableVariable<usize>,
290 #[visit(skip)]
292 #[reflect(hidden)]
293 pub drag_context: Option<DragContext<T>>,
294 pub drag_value_scaling: InheritableVariable<f32>,
296}
297
298impl<T: NumericType> ConstructorProvider<UiNode, UserInterface> for NumericUpDown<T> {
299 fn constructor() -> GraphNodeConstructor<UiNode, UserInterface> {
300 GraphNodeConstructor::new::<Self>()
301 .with_variant(
302 format!("Numeric Up Down<{}>", std::any::type_name::<T>()),
303 |ui| {
304 NumericUpDownBuilder::<T>::new(
305 WidgetBuilder::new().with_name("Numeric Up Down"),
306 )
307 .build(&mut ui.build_ctx())
308 .into()
309 },
310 )
311 .with_group("Numeric")
312 }
313}
314
315impl<T: NumericType> Deref for NumericUpDown<T> {
316 type Target = Widget;
317
318 fn deref(&self) -> &Self::Target {
319 &self.widget
320 }
321}
322
323impl<T: NumericType> DerefMut for NumericUpDown<T> {
324 fn deref_mut(&mut self) -> &mut Self::Target {
325 &mut self.widget
326 }
327}
328
329impl<T: NumericType> NumericUpDown<T> {
330 fn clamp_value(&self, value: T) -> T {
331 clamp(value, *self.min_value, *self.max_value)
332 }
333
334 fn sync_text_field(&mut self, ui: &UserInterface) {
335 let text = format!("{:.1$}", *self.value, *self.precision);
336 self.formatted_value = text.parse::<T>().unwrap_or(*self.value);
337 let msg = TextMessage::text(
338 *self.field,
339 MessageDirection::ToWidget,
340 format!("{:.1$}", *self.value, *self.precision),
341 );
342 msg.set_handled(true);
343 ui.send_message(msg);
344 }
345
346 fn sync_value_to_bounds_if_needed(&self, ui: &UserInterface) {
347 let clamped = self.clamp_value(*self.value);
348 if *self.value != clamped {
349 ui.send_message(NumericUpDownMessage::value(
350 self.handle,
351 MessageDirection::ToWidget,
352 clamped,
353 ));
354 }
355 }
356
357 fn try_parse_value(&mut self, ui: &UserInterface) {
358 if let Some(field) = ui.node(*self.field).cast::<TextBox>() {
360 if let Ok(value) = field.text().parse::<T>() {
361 if value != self.formatted_value {
365 self.formatted_value = value;
366 let value = self.clamp_value(value);
367 ui.send_message(NumericUpDownMessage::value(
368 self.handle(),
369 MessageDirection::ToWidget,
370 value,
371 ));
372 }
373 } else {
374 self.sync_text_field(ui);
376 }
377 }
378 }
379}
380
381fn saturating_sub<T>(a: T, b: T) -> T
382where
383 T: NumericType,
384{
385 assert!(b >= T::zero());
386
387 if a >= b + T::min_value() {
388 a - b
389 } else {
390 T::min_value()
391 }
392}
393
394fn saturating_add<T>(a: T, b: T) -> T
395where
396 T: NumericType,
397{
398 assert!(b >= T::zero());
399
400 if a < T::max_value() - b {
401 a + b
402 } else {
403 T::max_value()
404 }
405}
406
407fn calculate_value_by_offset<T: NumericType>(
408 start_value: T,
409 offset: i32,
410 step: T,
411 min: T,
412 max: T,
413) -> T {
414 let mut new_value = start_value;
415 match offset.cmp(&0) {
416 Ordering::Less => {
417 for _ in 0..(-offset) {
418 new_value = saturating_sub(new_value, step);
419 }
420 }
421 Ordering::Equal => {}
422 Ordering::Greater => {
423 for _ in 0..offset {
424 new_value = saturating_add(new_value, step);
425 }
426 }
427 }
428 new_value = clamp(new_value, min, max);
429 new_value
430}
431
432impl<T> TypeUuidProvider for NumericUpDown<T>
433where
434 T: NumericType,
435{
436 fn type_uuid() -> Uuid {
437 combine_uuids(
438 uuid!("f852eda4-18e5-4480-83ae-a607ce1c26f7"),
439 T::type_uuid(),
440 )
441 }
442}
443
444impl<T: NumericType> Control for NumericUpDown<T> {
445 fn handle_routed_message(&mut self, ui: &mut UserInterface, message: &mut UiMessage) {
446 self.widget.handle_routed_message(ui, message);
447
448 if let Some(TextMessage::Text(_)) = message.data() {
449 if message.destination() == *self.field
450 && message.direction == MessageDirection::FromWidget
451 && !message.handled()
452 {
453 self.try_parse_value(ui);
454 }
455 } else if let Some(msg) = message.data::<WidgetMessage>() {
456 match msg {
457 WidgetMessage::MouseDown { button, pos, .. } => {
458 if *button == MouseButton::Left
460 && (ui
461 .node(*self.increase)
462 .has_descendant(message.destination(), ui)
463 || ui
464 .node(*self.decrease)
465 .has_descendant(message.destination(), ui))
466 {
467 self.drag_context = Some(DragContext::PreDrag {
468 start_mouse_pos: pos.y,
469 });
470 }
471 }
472 WidgetMessage::MouseMove { pos, .. } => {
473 if let Some(drag_context) = self.drag_context.as_ref() {
474 match drag_context {
475 DragContext::PreDrag { start_mouse_pos } => {
476 if (pos.y - start_mouse_pos).abs() >= 5.0 {
477 self.drag_context = Some(DragContext::Dragging {
478 start_value: *self.value,
479 start_mouse_pos: *start_mouse_pos,
480 });
481 }
482 }
483 DragContext::Dragging {
484 start_value,
485 start_mouse_pos,
486 } => {
487 ui.send_message(TextMessage::text(
489 *self.field,
490 MessageDirection::ToWidget,
491 format!(
492 "{:.1$}",
493 calculate_value_by_offset(
494 *start_value,
495 ((*start_mouse_pos - pos.y) * *self.drag_value_scaling)
496 as i32,
497 *self.step,
498 *self.min_value,
499 *self.max_value
500 ),
501 *self.precision
502 ),
503 ));
504 }
505 }
506 }
507 }
508 WidgetMessage::KeyDown(key_code) => match *key_code {
509 KeyCode::ArrowUp => {
510 ui.send_message(ButtonMessage::click(
511 *self.increase,
512 MessageDirection::FromWidget,
513 ));
514 }
515 KeyCode::ArrowDown => {
516 ui.send_message(ButtonMessage::click(
517 *self.decrease,
518 MessageDirection::FromWidget,
519 ));
520 }
521 _ => (),
522 },
523 _ => {}
524 }
525 } else if let Some(msg) = message.data::<NumericUpDownMessage<T>>() {
526 if message.direction() == MessageDirection::ToWidget
527 && message.destination() == self.handle()
528 {
529 match msg {
530 NumericUpDownMessage::Value(value) => {
531 let clamped = self.clamp_value(*value);
532 if *self.value != clamped {
533 self.value.set_value_and_mark_modified(clamped);
534
535 self.sync_text_field(ui);
536
537 let mut msg = NumericUpDownMessage::value(
538 self.handle,
539 MessageDirection::FromWidget,
540 *self.value,
541 );
542 msg.set_handled(message.handled());
544 msg.flags = message.flags;
545 ui.send_message(msg);
546 }
547 }
548 NumericUpDownMessage::MinValue(min_value) => {
549 if (*self.min_value).ne(min_value) {
550 self.min_value.set_value_and_mark_modified(*min_value);
551 ui.send_message(message.reverse());
552 self.sync_value_to_bounds_if_needed(ui);
553 }
554 }
555 NumericUpDownMessage::MaxValue(max_value) => {
556 if (*self.max_value).ne(max_value) {
557 self.max_value.set_value_and_mark_modified(*max_value);
558 ui.send_message(message.reverse());
559 self.sync_value_to_bounds_if_needed(ui);
560 }
561 }
562 NumericUpDownMessage::Step(step) => {
563 if (*self.step).ne(step) {
564 self.step.set_value_and_mark_modified(*step);
565 ui.send_message(message.reverse());
566 self.sync_text_field(ui);
567 }
568 }
569 NumericUpDownMessage::Precision(precision) => {
570 if (*self.precision).ne(precision) {
571 self.precision.set_value_and_mark_modified(*precision);
572 ui.send_message(message.reverse());
573 self.sync_text_field(ui);
574 }
575 }
576 }
577 }
578 } else if let Some(ButtonMessage::Click) = message.data::<ButtonMessage>() {
579 if message.destination() == *self.decrease || message.destination() == *self.increase {
580 if let Some(DragContext::Dragging {
581 start_value,
582 start_mouse_pos,
583 }) = self.drag_context.take()
584 {
585 ui.send_message(NumericUpDownMessage::value(
586 self.handle,
587 MessageDirection::ToWidget,
588 calculate_value_by_offset(
589 start_value,
590 ((start_mouse_pos - ui.cursor_position().y) * *self.drag_value_scaling)
591 as i32,
592 *self.step,
593 *self.min_value,
594 *self.max_value,
595 ),
596 ));
597 } else if message.destination() == *self.decrease {
598 let value = self.clamp_value(saturating_sub(*self.value, *self.step));
599 ui.send_message(NumericUpDownMessage::value(
600 self.handle(),
601 MessageDirection::ToWidget,
602 value,
603 ));
604 } else if message.destination() == *self.increase {
605 let value = self.clamp_value(saturating_add(*self.value, *self.step));
606
607 ui.send_message(NumericUpDownMessage::value(
608 self.handle(),
609 MessageDirection::ToWidget,
610 value,
611 ));
612 }
613 }
614 }
615 }
616}
617
618pub struct NumericUpDownBuilder<T: NumericType> {
620 widget_builder: WidgetBuilder,
621 value: T,
622 step: T,
623 min_value: T,
624 max_value: T,
625 precision: usize,
626 editable: bool,
627 drag_value_scaling: f32,
628}
629
630fn make_button(
631 ctx: &mut BuildContext,
632 arrow: ArrowDirection,
633 row: usize,
634 editable: bool,
635) -> Handle<UiNode> {
636 let handle = ButtonBuilder::new(
637 WidgetBuilder::new()
638 .with_enabled(editable)
639 .with_margin(Thickness::right(1.0))
640 .on_row(row),
641 )
642 .with_back(
643 DecoratorBuilder::new(
644 BorderBuilder::new(
645 WidgetBuilder::new().with_foreground(ctx.style.property(Style::BRUSH_LIGHTER)),
646 )
647 .with_corner_radius(2.0f32.into())
648 .with_pad_by_corner_radius(false),
649 )
650 .with_normal_brush(ctx.style.property(Style::BRUSH_PRIMARY))
651 .with_hover_brush(ctx.style.property(Style::BRUSH_LIGHT))
652 .with_pressed_brush(ctx.style.property(Style::BRUSH_BRIGHT_BLUE))
653 .build(ctx),
654 )
655 .with_content(make_arrow(ctx, arrow, 6.0))
656 .build(ctx);
657
658 ctx[handle].accepts_input = false;
660
661 handle
662}
663
664impl<T: NumericType> NumericUpDownBuilder<T> {
665 pub fn new(widget_builder: WidgetBuilder) -> Self {
667 Self {
668 widget_builder,
669 value: T::zero(),
670 step: T::one(),
671 min_value: T::min_value(),
672 max_value: T::max_value(),
673 precision: 3,
674 editable: true,
675 drag_value_scaling: 0.1,
676 }
677 }
678
679 fn set_value(&mut self, value: T) {
680 self.value = clamp(value, self.min_value, self.max_value);
681 }
682
683 pub fn with_min_value(mut self, value: T) -> Self {
685 self.min_value = value;
686 self.set_value(self.value);
687 self
688 }
689
690 pub fn with_max_value(mut self, value: T) -> Self {
692 self.max_value = value;
693 self.set_value(self.value);
694 self
695 }
696
697 pub fn with_value(mut self, value: T) -> Self {
699 self.value = value;
700 self.set_value(value);
701 self
702 }
703
704 pub fn with_step(mut self, step: T) -> Self {
706 assert!(step >= T::zero());
707
708 self.step = step;
709 self
710 }
711
712 pub fn with_precision(mut self, precision: usize) -> Self {
714 self.precision = precision;
715 self
716 }
717
718 pub fn with_editable(mut self, editable: bool) -> Self {
720 self.editable = editable;
721 self
722 }
723
724 pub fn with_drag_value_scaling(mut self, drag_value_scaling: f32) -> Self {
727 self.drag_value_scaling = drag_value_scaling;
728 self
729 }
730
731 pub fn build(self, ctx: &mut BuildContext) -> Handle<UiNode> {
733 let increase;
734 let decrease;
735 let field;
736 let back = BorderBuilder::new(
737 WidgetBuilder::new()
738 .with_background(ctx.style.property(Style::BRUSH_DARK))
739 .with_foreground(ctx.style.property(Style::BRUSH_LIGHT)),
740 )
741 .with_corner_radius(4.0f32.into())
742 .with_pad_by_corner_radius(false)
743 .with_stroke_thickness(Thickness::uniform(1.0).into())
744 .build(ctx);
745
746 let text = format!("{:.1$}", self.value, self.precision);
747 let formatted_value = text.parse::<T>().unwrap_or(self.value);
748 let grid = GridBuilder::new(
749 WidgetBuilder::new()
750 .with_child({
751 field = TextBoxBuilder::new(
752 WidgetBuilder::new()
753 .on_row(0)
754 .on_column(0)
755 .with_margin(Thickness::left(2.0)),
756 )
757 .with_vertical_text_alignment(VerticalAlignment::Center)
758 .with_horizontal_text_alignment(HorizontalAlignment::Left)
759 .with_text_commit_mode(TextCommitMode::Changed)
760 .with_text(text)
761 .with_editable(self.editable)
762 .build(ctx);
763 field
764 })
765 .with_child(
766 GridBuilder::new(
767 WidgetBuilder::new()
768 .on_column(1)
769 .with_child({
770 increase = make_button(ctx, ArrowDirection::Top, 0, self.editable);
771 increase
772 })
773 .with_child({
774 decrease =
775 make_button(ctx, ArrowDirection::Bottom, 1, self.editable);
776 decrease
777 }),
778 )
779 .add_column(Column::auto())
780 .add_row(Row::stretch())
781 .add_row(Row::stretch())
782 .build(ctx),
783 ),
784 )
785 .add_row(Row::stretch())
786 .add_column(Column::stretch())
787 .add_column(Column::auto())
788 .build(ctx);
789
790 ctx.link(grid, back);
791
792 let node = NumericUpDown {
793 widget: self.widget_builder.with_child(back).build(ctx),
794 increase: increase.into(),
795 decrease: decrease.into(),
796 field: field.into(),
797 value: self.value.into(),
798 formatted_value,
799 step: self.step.into(),
800 min_value: self.min_value.into(),
801 max_value: self.max_value.into(),
802 precision: self.precision.into(),
803 drag_context: None,
804 drag_value_scaling: self.drag_value_scaling.into(),
805 };
806
807 ctx.add_node(UiNode::new(node))
808 }
809}
810
811#[cfg(test)]
812mod test {
813
814 use crate::numeric::NumericUpDownBuilder;
815 use crate::{
816 numeric::{saturating_add, saturating_sub},
817 test::test_widget_deletion,
818 widget::WidgetBuilder,
819 };
820
821 #[test]
822 fn test_saturating_add() {
823 assert_eq!(saturating_add(0, 1), 1);
825 assert_eq!(saturating_add(1, 0), 1);
826 assert_eq!(saturating_add(0, 0), 0);
827 assert_eq!(saturating_add(1, 1), 2);
828 assert_eq!(saturating_add(i32::MAX, 1), i32::MAX);
829 assert_eq!(saturating_add(i32::MIN, 1), i32::MIN + 1);
830
831 assert_eq!(saturating_add(0.0, 1.0), 1.0);
833 assert_eq!(saturating_add(1.0, 0.0), 1.0);
834 assert_eq!(saturating_add(f32::MAX, 1.0), f32::MAX);
835 assert_eq!(saturating_add(f32::MIN, 1.0), f32::MIN + 1.0);
836 }
837
838 #[test]
839 fn test_saturating_sub() {
840 assert_eq!(saturating_sub(0, 0), 0);
842 assert_eq!(saturating_sub(0, 1), -1);
843 assert_eq!(saturating_sub(1, 1), 0);
844 assert_eq!(saturating_sub(1, 0), 1);
845 assert_eq!(saturating_sub(10, 10), 0);
846 assert_eq!(saturating_sub(i32::MIN, 1), i32::MIN);
847 assert_eq!(saturating_sub(i32::MAX, 1), i32::MAX - 1);
848
849 assert_eq!(saturating_sub(0u32, 0u32), 0u32);
851 assert_eq!(saturating_sub(0u32, 1u32), 0u32);
852 assert_eq!(saturating_sub(1u32, 1u32), 0u32);
853 assert_eq!(saturating_sub(1u32, 0u32), 1u32);
854 assert_eq!(saturating_sub(10u32, 10u32), 0u32);
855 assert_eq!(saturating_sub(u32::MIN, 1u32), u32::MIN);
856 assert_eq!(saturating_sub(u32::MAX, 1u32), u32::MAX - 1);
857
858 assert_eq!(saturating_sub(0.0, 1.0), -1.0);
860 assert_eq!(saturating_sub(1.0, 0.0), 1.0);
861 assert_eq!(saturating_sub(1.0, 1.0), 0.0);
862 assert_eq!(saturating_sub(f32::MIN, 1.0), f32::MIN);
863 assert_eq!(saturating_sub(f32::MAX, 1.0), f32::MAX - 1.0);
864 }
865
866 #[test]
867 fn test_deletion() {
868 test_widget_deletion(|ctx| {
869 NumericUpDownBuilder::<f32>::new(WidgetBuilder::new()).build(ctx)
870 });
871 }
872}