diff --git a/demo/Ursa.Demo/Models/MenuKeys.cs b/demo/Ursa.Demo/Models/MenuKeys.cs
index 56e6a49..530349b 100644
--- a/demo/Ursa.Demo/Models/MenuKeys.cs
+++ b/demo/Ursa.Demo/Models/MenuKeys.cs
@@ -20,6 +20,7 @@ public static class MenuKeys
public const string MenuKeyMessageBox = "MessageBox";
public const string MenuKeyNavigation = "Navigation";
public const string MenuKeyNavMenu = "NavMenu";
+ public const string MenuKeyNumberDisplayer = "NumberDisplayer";
public const string MenuKeyNumericUpDown = "NumericUpDown";
public const string MenuKeyPagination = "Pagination";
public const string MenuKeyRangeSlider = "RangeSlider";
diff --git a/demo/Ursa.Demo/Pages/NumberDisplayerDemo.axaml b/demo/Ursa.Demo/Pages/NumberDisplayerDemo.axaml
new file mode 100644
index 0000000..0a7ec19
--- /dev/null
+++ b/demo/Ursa.Demo/Pages/NumberDisplayerDemo.axaml
@@ -0,0 +1,16 @@
+
+
+
+
+
+
+
diff --git a/demo/Ursa.Demo/Pages/NumberDisplayerDemo.axaml.cs b/demo/Ursa.Demo/Pages/NumberDisplayerDemo.axaml.cs
new file mode 100644
index 0000000..35df3ef
--- /dev/null
+++ b/demo/Ursa.Demo/Pages/NumberDisplayerDemo.axaml.cs
@@ -0,0 +1,13 @@
+using Avalonia;
+using Avalonia.Controls;
+using Avalonia.Markup.Xaml;
+
+namespace Ursa.Demo.Pages;
+
+public partial class NumberDisplayerDemo : UserControl
+{
+ public NumberDisplayerDemo()
+ {
+ InitializeComponent();
+ }
+}
\ No newline at end of file
diff --git a/demo/Ursa.Demo/ViewModels/MainViewViewModel.cs b/demo/Ursa.Demo/ViewModels/MainViewViewModel.cs
index 43166bd..d8c0cb4 100644
--- a/demo/Ursa.Demo/ViewModels/MainViewViewModel.cs
+++ b/demo/Ursa.Demo/ViewModels/MainViewViewModel.cs
@@ -42,6 +42,7 @@ public class MainViewViewModel : ViewModelBase
MenuKeys.MenuKeyMessageBox => new MessageBoxDemoViewModel(),
MenuKeys.MenuKeyNavigation => new NavigationMenuDemoViewModel(),
MenuKeys.MenuKeyNavMenu => new NavMenuDemoViewModel(),
+ MenuKeys.MenuKeyNumberDisplayer => new NumberDisplayerDemoViewModel(),
MenuKeys.MenuKeyNumericUpDown => new NumericUpDownDemoViewModel(),
MenuKeys.MenuKeyPagination => new PaginationDemoViewModel(),
MenuKeys.MenuKeyRangeSlider => new RangeSliderDemoViewModel(),
diff --git a/demo/Ursa.Demo/ViewModels/MenuViewModel.cs b/demo/Ursa.Demo/ViewModels/MenuViewModel.cs
index 6f6da25..c24db24 100644
--- a/demo/Ursa.Demo/ViewModels/MenuViewModel.cs
+++ b/demo/Ursa.Demo/ViewModels/MenuViewModel.cs
@@ -29,7 +29,8 @@ public class MenuViewModel: ViewModelBase
new() { MenuHeader = "Message Box", Key = MenuKeys.MenuKeyMessageBox, Status = "New" },
new() { MenuHeader = "Navigation", Key = MenuKeys.MenuKeyNavigation, Status = "WIP" },
new() { MenuHeader = "Nav Menu", Key = MenuKeys.MenuKeyNavMenu, Status = "WIP"},
- new() { MenuHeader = "NumericUpDown", Key = MenuKeys.MenuKeyNumericUpDown, Status = "New" },
+ new() { MenuHeader = "Number Displayer", Key = MenuKeys.MenuKeyNumberDisplayer, Status = "New" },
+ new() { MenuHeader = "Numeric UpDown", Key = MenuKeys.MenuKeyNumericUpDown, Status = "New" },
new() { MenuHeader = "Pagination", Key = MenuKeys.MenuKeyPagination },
new() { MenuHeader = "RangeSlider", Key = MenuKeys.MenuKeyRangeSlider, Status = "New"},
new() { MenuHeader = "Selection List", Key = MenuKeys.MenuKeySelectionList, Status = "New" },
diff --git a/demo/Ursa.Demo/ViewModels/NumberDisplayerDemoViewModel.cs b/demo/Ursa.Demo/ViewModels/NumberDisplayerDemoViewModel.cs
new file mode 100644
index 0000000..0032e47
--- /dev/null
+++ b/demo/Ursa.Demo/ViewModels/NumberDisplayerDemoViewModel.cs
@@ -0,0 +1,26 @@
+using System;
+using System.Windows.Input;
+using CommunityToolkit.Mvvm.ComponentModel;
+using CommunityToolkit.Mvvm.Input;
+
+namespace Ursa.Demo.ViewModels;
+
+public partial class NumberDisplayerDemoViewModel: ObservableObject
+{
+ [ObservableProperty] private int _value;
+ [ObservableProperty] private double _doubleValue;
+ public ICommand IncreaseCommand { get; }
+ public NumberDisplayerDemoViewModel()
+ {
+ IncreaseCommand = new RelayCommand(OnChange);
+ Value = 0;
+ DoubleValue = 0d;
+ }
+
+ private void OnChange()
+ {
+ Random r = new Random();
+ Value = r.Next(int.MaxValue);
+ DoubleValue = r.NextDouble() * 100000;
+ }
+}
\ No newline at end of file
diff --git a/src/Ursa.Themes.Semi/Controls/NumberDisplayer.axaml b/src/Ursa.Themes.Semi/Controls/NumberDisplayer.axaml
new file mode 100644
index 0000000..b5f5d8e
--- /dev/null
+++ b/src/Ursa.Themes.Semi/Controls/NumberDisplayer.axaml
@@ -0,0 +1,15 @@
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/Ursa.Themes.Semi/Controls/_index.axaml b/src/Ursa.Themes.Semi/Controls/_index.axaml
index 0b2dae5..dee8aee 100644
--- a/src/Ursa.Themes.Semi/Controls/_index.axaml
+++ b/src/Ursa.Themes.Semi/Controls/_index.axaml
@@ -20,6 +20,7 @@
+
diff --git a/src/Ursa/Controls/NumberDisplayer/NumberDisplayer.cs b/src/Ursa/Controls/NumberDisplayer/NumberDisplayer.cs
new file mode 100644
index 0000000..181e955
--- /dev/null
+++ b/src/Ursa/Controls/NumberDisplayer/NumberDisplayer.cs
@@ -0,0 +1,119 @@
+using Avalonia;
+using Avalonia.Animation;
+using Avalonia.Controls;
+using Avalonia.Controls.Primitives;
+using Avalonia.Data;
+
+namespace Ursa.Controls;
+
+public abstract class NumberDisplayerBase : TemplatedControl
+{
+ public static readonly DirectProperty InternalTextProperty = AvaloniaProperty.RegisterDirect(
+ nameof(InternalText), o => o.InternalText, (o, v) => o.InternalText = v);
+ private string _internalText;
+ public string InternalText
+ {
+ get => _internalText;
+ set => SetAndRaise(InternalTextProperty, ref _internalText, value);
+ }
+
+ public static readonly StyledProperty DurationProperty = AvaloniaProperty.Register(
+ nameof(Duration));
+
+ public TimeSpan Duration
+ {
+ get => GetValue(DurationProperty);
+ set => SetValue(DurationProperty, value);
+ }
+
+ public static readonly StyledProperty StringFormatProperty = AvaloniaProperty.Register(
+ nameof(StringFormat));
+
+ public string StringFormat
+ {
+ get => GetValue(StringFormatProperty);
+ set => SetValue(StringFormatProperty, value);
+ }
+}
+
+public abstract class NumberDisplayer: NumberDisplayerBase
+{
+ public static readonly StyledProperty ValueProperty = AvaloniaProperty.Register, T?>(
+ nameof(Value), defaultBindingMode:BindingMode.TwoWay);
+
+ public T? Value
+ {
+ get => GetValue(ValueProperty);
+ set => SetValue(ValueProperty, value);
+ }
+
+ public static readonly StyledProperty InternalValueProperty = AvaloniaProperty.Register, T?>(
+ nameof(InternalValue));
+
+ public T? InternalValue
+ {
+ get => GetValue(InternalValueProperty);
+ set => SetValue(InternalValueProperty, value);
+ }
+
+ static NumberDisplayer()
+ {
+ ValueProperty.Changed.AddClassHandler, T?>((item, args) =>
+ {
+ item.InternalValue = args.NewValue.Value;
+ });
+ InternalValueProperty.Changed.AddClassHandler, T?>((item, args) =>
+ {
+ item.InternalText = args.NewValue.Value is null ? string.Empty : item.GetString(args.NewValue.Value);
+ });
+ DurationProperty.Changed.AddClassHandler, TimeSpan>((item, args) =>item.OnDurationChanged(args));
+ }
+
+ private void OnDurationChanged(AvaloniaPropertyChangedEventArgs args)
+ {
+ this.Transitions ??= new Transitions();
+ this.Transitions?.Clear();
+ this.Transitions?.Add(GetTransition(args.NewValue.Value));
+ }
+
+ protected abstract ITransition GetTransition(TimeSpan duration);
+ protected abstract string GetString(T? value);
+}
+
+public class Int32Displayer : NumberDisplayer
+{
+ protected override Type StyleKeyOverride { get; } = typeof(NumberDisplayerBase);
+
+ protected override ITransition GetTransition(TimeSpan duration)
+ {
+ return new IntegerTransition()
+ {
+ Property = InternalValueProperty,
+ Duration = duration
+ };
+ }
+
+ protected override string GetString(int value)
+ {
+ return value.ToString(StringFormat);
+ }
+}
+
+public class DoubleDisplayer : NumberDisplayer
+{
+ protected override Type StyleKeyOverride { get; } = typeof(NumberDisplayerBase);
+
+ protected override ITransition GetTransition(TimeSpan duration)
+ {
+ return new DoubleTransition()
+ {
+ Property = InternalValueProperty,
+ Duration = duration
+ };
+ }
+
+ protected override string GetString(double value)
+ {
+ return value.ToString(StringFormat);
+ }
+}