diff --git a/demo/Ursa.Demo/Models/MenuKeys.cs b/demo/Ursa.Demo/Models/MenuKeys.cs
index 1b88691..b3e2753 100644
--- a/demo/Ursa.Demo/Models/MenuKeys.cs
+++ b/demo/Ursa.Demo/Models/MenuKeys.cs
@@ -15,6 +15,7 @@ public static class MenuKeys
public const string MenuKeyLoading = "Loading";
public const string MenuKeyMessageBox = "MessageBox";
public const string MenuKeyNavigation = "Navigation";
+ public const string MenuKeyNumericUpDown = "NumericUpDown";
public const string MenuKeyPagination = "Pagination";
public const string MenuKeyTagInput = "TagInput";
public const string MenuKeyTimeline = "Timeline";
diff --git a/demo/Ursa.Demo/Pages/NumericUpDownDemo.axaml b/demo/Ursa.Demo/Pages/NumericUpDownDemo.axaml
new file mode 100644
index 0000000..afe4496
--- /dev/null
+++ b/demo/Ursa.Demo/Pages/NumericUpDownDemo.axaml
@@ -0,0 +1,26 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/demo/Ursa.Demo/Pages/NumericUpDownDemo.axaml.cs b/demo/Ursa.Demo/Pages/NumericUpDownDemo.axaml.cs
new file mode 100644
index 0000000..4bdf06d
--- /dev/null
+++ b/demo/Ursa.Demo/Pages/NumericUpDownDemo.axaml.cs
@@ -0,0 +1,15 @@
+using Avalonia;
+using Avalonia.Controls;
+using Avalonia.Markup.Xaml;
+using Ursa.Demo.ViewModels;
+
+namespace Ursa.Demo.Pages;
+
+public partial class NumericUpDownDemo : UserControl
+{
+ public NumericUpDownDemo()
+ {
+ InitializeComponent();
+ DataContext = new NumericUpDownDemoViewModel();
+ }
+}
\ No newline at end of file
diff --git a/demo/Ursa.Demo/ViewModels/MainViewViewModel.cs b/demo/Ursa.Demo/ViewModels/MainViewViewModel.cs
index 83fea1a..85312ef 100644
--- a/demo/Ursa.Demo/ViewModels/MainViewViewModel.cs
+++ b/demo/Ursa.Demo/ViewModels/MainViewViewModel.cs
@@ -37,6 +37,7 @@ public class MainViewViewModel : ViewModelBase
MenuKeys.MenuKeyLoading => new LoadingDemoViewModel(),
MenuKeys.MenuKeyMessageBox => new MessageBoxDemoViewModel(),
MenuKeys.MenuKeyNavigation => new NavigationMenuDemoViewModel(),
+ MenuKeys.MenuKeyNumericUpDown => new NumericUpDownDemoViewModel(),
MenuKeys.MenuKeyPagination => new PaginationDemoViewModel(),
MenuKeys.MenuKeyTagInput => new TagInputDemoViewModel(),
MenuKeys.MenuKeyTimeline => new TimelineDemoViewModel(),
diff --git a/demo/Ursa.Demo/ViewModels/MenuViewModel.cs b/demo/Ursa.Demo/ViewModels/MenuViewModel.cs
index c321499..ea66a5e 100644
--- a/demo/Ursa.Demo/ViewModels/MenuViewModel.cs
+++ b/demo/Ursa.Demo/ViewModels/MenuViewModel.cs
@@ -24,6 +24,7 @@ public class MenuViewModel: ViewModelBase
new() { MenuHeader = "Loading", Key = MenuKeys.MenuKeyLoading },
new() { MenuHeader = "Message Box", Key = MenuKeys.MenuKeyMessageBox },
new() { MenuHeader = "Navigation", Key = MenuKeys.MenuKeyNavigation },
+ new() { MenuHeader = "NumericUpDown", Key = MenuKeys.MenuKeyNumericUpDown },
new() { MenuHeader = "Pagination", Key = MenuKeys.MenuKeyPagination },
new() { MenuHeader = "TagInput", Key = MenuKeys.MenuKeyTagInput },
new() { MenuHeader = "Timeline", Key = MenuKeys.MenuKeyTimeline },
diff --git a/demo/Ursa.Demo/ViewModels/NumericUpDownDemoViewModel.cs b/demo/Ursa.Demo/ViewModels/NumericUpDownDemoViewModel.cs
new file mode 100644
index 0000000..9840cca
--- /dev/null
+++ b/demo/Ursa.Demo/ViewModels/NumericUpDownDemoViewModel.cs
@@ -0,0 +1,8 @@
+using CommunityToolkit.Mvvm.ComponentModel;
+
+namespace Ursa.Demo.ViewModels;
+
+public class NumericUpDownDemoViewModel: ObservableObject
+{
+
+}
\ No newline at end of file
diff --git a/src/Ursa.Themes.Semi/Controls/NumericUpDown.axaml b/src/Ursa.Themes.Semi/Controls/NumericUpDown.axaml
new file mode 100644
index 0000000..31c1aa8
--- /dev/null
+++ b/src/Ursa.Themes.Semi/Controls/NumericUpDown.axaml
@@ -0,0 +1,87 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/Ursa.Themes.Semi/Controls/_index.axaml b/src/Ursa.Themes.Semi/Controls/_index.axaml
index 1fbacb9..edf9abb 100644
--- a/src/Ursa.Themes.Semi/Controls/_index.axaml
+++ b/src/Ursa.Themes.Semi/Controls/_index.axaml
@@ -13,6 +13,7 @@
+
diff --git a/src/Ursa/Controls/NumericUpDown/IntUpDown.cs b/src/Ursa/Controls/NumericUpDown/IntUpDown.cs
new file mode 100644
index 0000000..4feedcb
--- /dev/null
+++ b/src/Ursa/Controls/NumericUpDown/IntUpDown.cs
@@ -0,0 +1,236 @@
+using System.Globalization;
+using Avalonia.Controls;
+using Avalonia.Utilities;
+
+namespace Ursa.Controls;
+
+public class NumericIntUpDown : NumericUpDownBase
+{
+ protected override Type StyleKeyOverride { get; } = typeof(NumericUpDown);
+
+ static NumericIntUpDown()
+ {
+ MaximumProperty.OverrideDefaultValue(int.MaxValue);
+ MinimumProperty.OverrideDefaultValue(int.MinValue);
+ StepProperty.OverrideDefaultValue(1);
+ }
+
+ protected override bool ParseText(string? text, out int number) =>
+ int.TryParse(text, ParsingNumberStyle, NumberFormat, out number);
+
+ protected override string? ValueToString(int? value) => value?.ToString(FormatString, NumberFormat);
+
+ protected override int Zero => 0;
+
+ protected override int? Add(int? a, int? b) => a + b;
+
+ protected override int? Minus(int? a, int? b) => a - b;
+}
+
+public class NumericDoubleUpDown : NumericUpDownBase
+{
+ protected override Type StyleKeyOverride { get; } = typeof(NumericUpDown);
+
+ static NumericDoubleUpDown()
+ {
+ MaximumProperty.OverrideDefaultValue(double.MaxValue);
+ MinimumProperty.OverrideDefaultValue(double.MinValue);
+ StepProperty.OverrideDefaultValue(1);
+ }
+
+ protected override bool ParseText(string? text, out double number) =>
+ double.TryParse(text, ParsingNumberStyle, NumberFormat, out number);
+
+ protected override string? ValueToString(double? value) => value?.ToString(FormatString, NumberFormat);
+
+ protected override double Zero => 0;
+
+ protected override double? Add(double? a, double? b) => a + b;
+
+ protected override double? Minus(double? a, double? b) => a - b;
+}
+
+public class NumericByteUpDown : NumericUpDownBase
+{
+ protected override Type StyleKeyOverride { get; } = typeof(NumericUpDown);
+
+ static NumericByteUpDown()
+ {
+ MaximumProperty.OverrideDefaultValue(byte.MaxValue);
+ MinimumProperty.OverrideDefaultValue(byte.MinValue);
+ StepProperty.OverrideDefaultValue(1);
+ }
+
+ protected override bool ParseText(string? text, out byte number) =>
+ byte.TryParse(text, ParsingNumberStyle, NumberFormat, out number);
+
+ protected override string? ValueToString(byte? value) => value?.ToString(FormatString, NumberFormat);
+
+ protected override byte Zero => 0;
+
+ protected override byte? Add(byte? a, byte? b) => (byte?) (a + b);
+
+ protected override byte? Minus(byte? a, byte? b) => (byte?) (a - b);
+}
+
+public class NumericSByteUpDown : NumericUpDownBase
+{
+ protected override Type StyleKeyOverride { get; } = typeof(NumericUpDown);
+
+ static NumericSByteUpDown()
+ {
+ MaximumProperty.OverrideDefaultValue(sbyte.MaxValue);
+ MinimumProperty.OverrideDefaultValue(sbyte.MinValue);
+ StepProperty.OverrideDefaultValue(1);
+ }
+
+ protected override bool ParseText(string? text, out sbyte number) =>
+ sbyte.TryParse(text, ParsingNumberStyle, NumberFormat, out number);
+
+ protected override string? ValueToString(sbyte? value) => value?.ToString(FormatString, NumberFormat);
+
+ protected override sbyte Zero => 0;
+
+ protected override sbyte? Add(sbyte? a, sbyte? b) => (sbyte?) (a + b);
+
+ protected override sbyte? Minus(sbyte? a, sbyte? b) => (sbyte?) (a - b);
+}
+
+public class NumericShortUpDown : NumericUpDownBase
+{
+ protected override Type StyleKeyOverride { get; } = typeof(NumericUpDown);
+
+ static NumericShortUpDown()
+ {
+ MaximumProperty.OverrideDefaultValue(short.MaxValue);
+ MinimumProperty.OverrideDefaultValue(short.MinValue);
+ StepProperty.OverrideDefaultValue(1);
+ }
+
+ protected override bool ParseText(string? text, out short number) =>
+ short.TryParse(text, ParsingNumberStyle, NumberFormat, out number);
+
+ protected override string? ValueToString(short? value) => value?.ToString(FormatString, NumberFormat);
+
+ protected override short Zero => 0;
+
+ protected override short? Add(short? a, short? b) => (short?) (a + b);
+
+ protected override short? Minus(short? a, short? b) => (short?) (a - b);
+}
+
+public class NumericUShortUpDown : NumericUpDownBase
+{
+ protected override Type StyleKeyOverride { get; } = typeof(NumericUpDown);
+
+ static NumericUShortUpDown()
+ {
+ MaximumProperty.OverrideDefaultValue(ushort.MaxValue);
+ MinimumProperty.OverrideDefaultValue(ushort.MinValue);
+ StepProperty.OverrideDefaultValue(1);
+ }
+
+ protected override bool ParseText(string? text, out ushort number) =>
+ ushort.TryParse(text, ParsingNumberStyle, NumberFormat, out number);
+
+ protected override string? ValueToString(ushort? value) => value?.ToString(FormatString, NumberFormat);
+
+ protected override ushort Zero => 0;
+
+ protected override ushort? Add(ushort? a, ushort? b) => (ushort?) (a + b);
+
+ protected override ushort? Minus(ushort? a, ushort? b) => (ushort?) (a - b);
+}
+
+public class NumericLongUpDown : NumericUpDownBase
+{
+ protected override Type StyleKeyOverride { get; } = typeof(NumericUpDown);
+
+ static NumericLongUpDown()
+ {
+ MaximumProperty.OverrideDefaultValue(long.MaxValue);
+ MinimumProperty.OverrideDefaultValue(long.MinValue);
+ StepProperty.OverrideDefaultValue(1);
+ }
+
+ protected override bool ParseText(string? text, out long number) =>
+ long.TryParse(text, ParsingNumberStyle, NumberFormat, out number);
+
+ protected override string? ValueToString(long? value) => value?.ToString(FormatString, NumberFormat);
+
+ protected override long Zero => 0;
+
+ protected override long? Add(long? a, long? b) => a + b;
+
+ protected override long? Minus(long? a, long? b) => a - b;
+}
+
+public class NumericULongUpDown : NumericUpDownBase
+{
+ protected override Type StyleKeyOverride { get; } = typeof(NumericUpDown);
+
+ static NumericULongUpDown()
+ {
+ MaximumProperty.OverrideDefaultValue(ulong.MaxValue);
+ MinimumProperty.OverrideDefaultValue(ulong.MinValue);
+ StepProperty.OverrideDefaultValue(1);
+ }
+
+ protected override bool ParseText(string? text, out ulong number) =>
+ ulong.TryParse(text, ParsingNumberStyle, NumberFormat, out number);
+
+ protected override string? ValueToString(ulong? value) => value?.ToString(FormatString, NumberFormat);
+
+ protected override ulong Zero => 0;
+
+ protected override ulong? Add(ulong? a, ulong? b) => a + b;
+
+ protected override ulong? Minus(ulong? a, ulong? b) => a - b;
+}
+
+public class NumericFloatUpDown : NumericUpDownBase
+{
+ protected override Type StyleKeyOverride { get; } = typeof(NumericUpDown);
+
+ static NumericFloatUpDown()
+ {
+ MaximumProperty.OverrideDefaultValue(float.MaxValue);
+ MinimumProperty.OverrideDefaultValue(float.MinValue);
+ StepProperty.OverrideDefaultValue(1);
+ }
+
+ protected override bool ParseText(string? text, out float number) =>
+ float.TryParse(text, ParsingNumberStyle, NumberFormat, out number);
+
+ protected override string? ValueToString(float? value) => value?.ToString(FormatString, NumberFormat);
+
+ protected override float Zero => 0;
+
+ protected override float? Add(float? a, float? b) => a + b;
+
+ protected override float? Minus(float? a, float? b) => a - b;
+}
+
+public class NumericDecimalUpDown : NumericUpDownBase
+{
+ protected override Type StyleKeyOverride { get; } = typeof(NumericUpDown);
+
+ static NumericDecimalUpDown()
+ {
+ MaximumProperty.OverrideDefaultValue(decimal.MaxValue);
+ MinimumProperty.OverrideDefaultValue(decimal.MinValue);
+ StepProperty.OverrideDefaultValue(1);
+ }
+
+ protected override bool ParseText(string? text, out decimal number) =>
+ decimal.TryParse(text, ParsingNumberStyle, NumberFormat, out number);
+
+ protected override string? ValueToString(decimal? value) => value?.ToString(FormatString, NumberFormat);
+
+ protected override decimal Zero => 0;
+
+ protected override decimal? Add(decimal? a, decimal? b) => a + b;
+
+ protected override decimal? Minus(decimal? a, decimal? b) => a - b;
+}
+
diff --git a/src/Ursa/Controls/NumericUpDown/NumericUpDownBase.cs b/src/Ursa/Controls/NumericUpDown/NumericUpDownBase.cs
new file mode 100644
index 0000000..f51e43a
--- /dev/null
+++ b/src/Ursa/Controls/NumericUpDown/NumericUpDownBase.cs
@@ -0,0 +1,614 @@
+using System.Diagnostics;
+using System.Globalization;
+using System.Net.Mime;
+using Avalonia;
+using Avalonia.Controls;
+using Avalonia.Controls.Metadata;
+using Avalonia.Controls.Primitives;
+using Avalonia.Data.Converters;
+using Avalonia.Input;
+using Avalonia.Interactivity;
+
+namespace Ursa.Controls;
+
+[TemplatePart(PART_Spinner, typeof(ButtonSpinner))]
+[TemplatePart(PART_TextBox, typeof(TextBox))]
+[TemplatePart(PART_DragPanel, typeof(Panel))]
+public abstract class NumericUpDown : TemplatedControl
+{
+ public const string PART_Spinner = "PART_Spinner";
+ public const string PART_TextBox = "PART_TextBox";
+ public const string PART_DragPanel = "PART_DragPanel";
+
+ protected internal ButtonSpinner? _spinner;
+ protected internal TextBox? _textBox;
+ protected internal Panel? _dragPanel;
+
+ private Point? _point;
+ protected internal bool _updateFromTextInput;
+
+ public static readonly StyledProperty AllowDragProperty = AvaloniaProperty.Register(
+ nameof(AllowDrag), defaultValue: false);
+
+ public bool AllowDrag
+ {
+ get => GetValue(AllowDragProperty);
+ set => SetValue(AllowDragProperty, value);
+ }
+
+ public static readonly StyledProperty IsReadOnlyProperty = AvaloniaProperty.Register(
+ nameof(IsReadOnly));
+
+ public bool IsReadOnly
+ {
+ get => GetValue(IsReadOnlyProperty);
+ set => SetValue(IsReadOnlyProperty, value);
+ }
+
+ public static readonly StyledProperty