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