diff --git a/demo/Ursa.Demo/Pages/VerificationCodeDemo.axaml b/demo/Ursa.Demo/Pages/VerificationCodeDemo.axaml
new file mode 100644
index 0000000..ecc21d6
--- /dev/null
+++ b/demo/Ursa.Demo/Pages/VerificationCodeDemo.axaml
@@ -0,0 +1,13 @@
+
+
+
+
+
+
+
diff --git a/demo/Ursa.Demo/Pages/VerificationCodeDemo.axaml.cs b/demo/Ursa.Demo/Pages/VerificationCodeDemo.axaml.cs
new file mode 100644
index 0000000..6ac89e7
--- /dev/null
+++ b/demo/Ursa.Demo/Pages/VerificationCodeDemo.axaml.cs
@@ -0,0 +1,13 @@
+using Avalonia;
+using Avalonia.Controls;
+using Avalonia.Markup.Xaml;
+
+namespace Ursa.Demo.Pages;
+
+public partial class VerificationCodeDemo : UserControl
+{
+ public VerificationCodeDemo()
+ {
+ InitializeComponent();
+ }
+}
\ No newline at end of file
diff --git a/demo/Ursa.Demo/ViewModels/MainViewViewModel.cs b/demo/Ursa.Demo/ViewModels/MainViewViewModel.cs
index 5a0cd77..aa9f90a 100644
--- a/demo/Ursa.Demo/ViewModels/MainViewViewModel.cs
+++ b/demo/Ursa.Demo/ViewModels/MainViewViewModel.cs
@@ -56,6 +56,7 @@ public class MainViewViewModel : ViewModelBase
MenuKeys.MenuKeyTwoTonePathIcon => new TwoTonePathIconDemoViewModel(),
MenuKeys.MenuKeyThemeToggler => new ThemeTogglerDemoViewModel(),
MenuKeys.MenuKeyToolBar => new ToolBarDemoViewModel(),
+ MenuKeys.MenuKeyVerificationCode => new VerificationCodeDemoViewModel(),
};
}
}
\ No newline at end of file
diff --git a/demo/Ursa.Demo/ViewModels/MenuViewModel.cs b/demo/Ursa.Demo/ViewModels/MenuViewModel.cs
index a48238e..1fa6c8a 100644
--- a/demo/Ursa.Demo/ViewModels/MenuViewModel.cs
+++ b/demo/Ursa.Demo/ViewModels/MenuViewModel.cs
@@ -42,7 +42,8 @@ public class MenuViewModel: ViewModelBase
new() { MenuHeader = "Theme Toggler", Key = MenuKeys.MenuKeyThemeToggler, Status = "New" },
new() { MenuHeader = "Timeline", Key = MenuKeys.MenuKeyTimeline, Status = "WIP" },
new() { MenuHeader = "TwoTonePathIcon", Key = MenuKeys.MenuKeyTwoTonePathIcon},
- new() { MenuHeader = "ToolBar", Key = MenuKeys.MenuKeyToolBar, Status = "New" }
+ new() { MenuHeader = "ToolBar", Key = MenuKeys.MenuKeyToolBar, Status = "New" },
+ new() { MenuHeader = "Verification Code", Key = MenuKeys.MenuKeyVerificationCode, Status = "New" },
};
}
}
@@ -81,5 +82,6 @@ public static class MenuKeys
public const string MenuKeyTwoTonePathIcon = "TwoTonePathIcon";
public const string MenuKeyThemeToggler = "ThemeToggler";
public const string MenuKeyToolBar = "ToolBar";
+ public const string MenuKeyVerificationCode = "VerificationCode";
}
\ No newline at end of file
diff --git a/demo/Ursa.Demo/ViewModels/VerificationCodeDemoViewModel.cs b/demo/Ursa.Demo/ViewModels/VerificationCodeDemoViewModel.cs
new file mode 100644
index 0000000..6f106e3
--- /dev/null
+++ b/demo/Ursa.Demo/ViewModels/VerificationCodeDemoViewModel.cs
@@ -0,0 +1,8 @@
+using CommunityToolkit.Mvvm.ComponentModel;
+
+namespace Ursa.Demo.ViewModels;
+
+public class VerificationCodeDemoViewModel: ObservableObject
+{
+
+}
\ No newline at end of file
diff --git a/src/Ursa.Themes.Semi/Controls/VerificationCode.axaml b/src/Ursa.Themes.Semi/Controls/VerificationCode.axaml
new file mode 100644
index 0000000..74e6993
--- /dev/null
+++ b/src/Ursa.Themes.Semi/Controls/VerificationCode.axaml
@@ -0,0 +1,34 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/Ursa.Themes.Semi/Controls/_index.axaml b/src/Ursa.Themes.Semi/Controls/_index.axaml
index ddb53fd..e79e02f 100644
--- a/src/Ursa.Themes.Semi/Controls/_index.axaml
+++ b/src/Ursa.Themes.Semi/Controls/_index.axaml
@@ -33,5 +33,6 @@
+
diff --git a/src/Ursa/Controls/VerificationCode/VerificationCode.cs b/src/Ursa/Controls/VerificationCode/VerificationCode.cs
new file mode 100644
index 0000000..efbc150
--- /dev/null
+++ b/src/Ursa/Controls/VerificationCode/VerificationCode.cs
@@ -0,0 +1,117 @@
+using System.Windows.Input;
+using Avalonia;
+using Avalonia.Collections;
+using Avalonia.Controls;
+using Avalonia.Controls.Metadata;
+using Avalonia.Controls.Presenters;
+using Avalonia.Controls.Primitives;
+using Avalonia.Input;
+using Avalonia.Interactivity;
+using Avalonia.LogicalTree;
+using Irihi.Avalonia.Shared.Helpers;
+
+namespace Ursa.Controls;
+
+[TemplatePart(PART_ItemsControl, typeof(ItemsControl))]
+public class VerificationCode: TemplatedControl
+{
+ public const string PART_ItemsControl = "PART_ItemsControl";
+ private ItemsControl? _itemsControl;
+ private int _currentIndex = 0;
+
+ public static readonly StyledProperty CompleteCommandProperty = AvaloniaProperty.Register(
+ nameof(CompleteCommand));
+
+ public ICommand? CompleteCommand
+ {
+ get => GetValue(CompleteCommandProperty);
+ set => SetValue(CompleteCommandProperty, value);
+ }
+
+ public static readonly StyledProperty CountOfDigitProperty = AvaloniaProperty.Register(
+ nameof(CountOfDigit));
+
+ public int CountOfDigit
+ {
+ get => GetValue(CountOfDigitProperty);
+ set => SetValue(CountOfDigitProperty, value);
+ }
+
+ public static readonly StyledProperty PasswordCharProperty =
+ AvaloniaProperty.Register(
+ nameof(PasswordChar));
+
+ public char PasswordChar
+ {
+ get => GetValue(PasswordCharProperty);
+ set => SetValue(PasswordCharProperty, value);
+ }
+
+ public static readonly DirectProperty> DigitsProperty = AvaloniaProperty.RegisterDirect>(
+ nameof(Digits), o => o.Digits, (o, v) => o.Digits = v);
+
+ private AvaloniaList _digits = [];
+ internal AvaloniaList Digits
+ {
+ get => _digits;
+ set => SetAndRaise(DigitsProperty, ref _digits, value);
+ }
+
+ static VerificationCode()
+ {
+ CountOfDigitProperty.Changed.AddClassHandler((code, args) => code.OnCountOfDigitChanged(args));
+ FocusableProperty.OverrideDefaultValue(true);
+ }
+
+ public VerificationCode()
+ {
+ InputMethod.SetIsInputMethodEnabled(this, false);
+ }
+
+ private void OnCountOfDigitChanged(AvaloniaPropertyChangedEventArgs args)
+ {
+ var newValue = args.NewValue.Value;
+ if (newValue > 0)
+ {
+ Digits = new AvaloniaList(Enumerable.Repeat(string.Empty, newValue));
+ }
+
+ }
+
+ protected override void OnApplyTemplate(TemplateAppliedEventArgs e)
+ {
+ base.OnApplyTemplate(e);
+ _itemsControl = e.NameScope.Get(PART_ItemsControl);
+ PointerPressedEvent.AddHandler(OnControlPressed, RoutingStrategies.Tunnel, false, this);
+ }
+
+ private void OnControlPressed(object sender, PointerPressedEventArgs e)
+ {
+ if (e.Source is Control t)
+ {
+ var text = t.FindLogicalAncestorOfType();
+ if (text != null)
+ {
+ _currentIndex = _itemsControl?.IndexFromContainer(text) ?? 0;
+ }
+ }
+ }
+
+ protected override void OnTextInput(TextInputEventArgs e)
+ {
+ base.OnTextInput(e);
+ if (e.Text?.Length == 1 && _currentIndex < CountOfDigit)
+ {
+ Digits[_currentIndex] = e.Text;
+ var presenter = _itemsControl?.ContainerFromIndex(_currentIndex) as TextBox;
+ if (presenter is null) return;
+ _currentIndex++;
+ var newPresenter = _itemsControl?.ContainerFromIndex(_currentIndex)?.Focus();
+ presenter.Text = e.Text;
+ if (_currentIndex == CountOfDigit)
+ {
+ CompleteCommand?.Execute(Digits);
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/Ursa/Controls/VerificationCode/VerificationCodeCollection.cs b/src/Ursa/Controls/VerificationCode/VerificationCodeCollection.cs
new file mode 100644
index 0000000..333af44
--- /dev/null
+++ b/src/Ursa/Controls/VerificationCode/VerificationCodeCollection.cs
@@ -0,0 +1,25 @@
+using Avalonia;
+using Avalonia.Controls;
+using Avalonia.Controls.Presenters;
+using Avalonia.Data;
+using Avalonia.Input;
+using Avalonia.Media;
+
+namespace Ursa.Controls;
+
+public class VerificationCodeCollection: ItemsControl
+{
+ protected override bool NeedsContainerOverride(object? item, int index, out object? recycleKey)
+ {
+ return NeedsContainer(item, out recycleKey);
+ }
+
+ protected override Control CreateContainerForItemOverride(object? item, int index, object? recycleKey)
+ {
+ return new TextBox()
+ {
+ TextAlignment = TextAlignment.Center,
+ [InputMethod.IsInputMethodEnabledProperty] = false,
+ };
+ }
+}
\ No newline at end of file