diff --git a/demo/Ursa.Demo/Models/MenuKeys.cs b/demo/Ursa.Demo/Models/MenuKeys.cs
index 2b30912..7f0c655 100644
--- a/demo/Ursa.Demo/Models/MenuKeys.cs
+++ b/demo/Ursa.Demo/Models/MenuKeys.cs
@@ -6,6 +6,7 @@ public static class MenuKeys
public const string MenuKeyBadge = "Badge";
public const string MenuKeyBanner = "Banner";
public const string MenuKeyButtonGroup = "ButtonGroup";
+ public const string MenuKeyClassInput = "Class Input";
public const string MenuKeyDialog = "Dialog";
public const string MenuKeyDivider = "Divider";
public const string MenuKeyDualBadge = "DualBadge";
diff --git a/demo/Ursa.Demo/Pages/BannerDemo.axaml b/demo/Ursa.Demo/Pages/BannerDemo.axaml
index ac539e7..d7a275e 100644
--- a/demo/Ursa.Demo/Pages/BannerDemo.axaml
+++ b/demo/Ursa.Demo/Pages/BannerDemo.axaml
@@ -47,6 +47,5 @@
-
diff --git a/demo/Ursa.Demo/Pages/ClassInputDemo.axaml b/demo/Ursa.Demo/Pages/ClassInputDemo.axaml
new file mode 100644
index 0000000..fc51e6d
--- /dev/null
+++ b/demo/Ursa.Demo/Pages/ClassInputDemo.axaml
@@ -0,0 +1,16 @@
+
+
+
+
+
+
+
+
+
+
diff --git a/demo/Ursa.Demo/Pages/ClassInputDemo.axaml.cs b/demo/Ursa.Demo/Pages/ClassInputDemo.axaml.cs
new file mode 100644
index 0000000..30e502a
--- /dev/null
+++ b/demo/Ursa.Demo/Pages/ClassInputDemo.axaml.cs
@@ -0,0 +1,13 @@
+using Avalonia;
+using Avalonia.Controls;
+using Avalonia.Markup.Xaml;
+
+namespace Ursa.Demo.Pages;
+
+public partial class ClassInputDemo : UserControl
+{
+ public ClassInputDemo()
+ {
+ InitializeComponent();
+ }
+}
\ No newline at end of file
diff --git a/demo/Ursa.Demo/ViewModels/ClassInputDemoViewModel.cs b/demo/Ursa.Demo/ViewModels/ClassInputDemoViewModel.cs
new file mode 100644
index 0000000..971131f
--- /dev/null
+++ b/demo/Ursa.Demo/ViewModels/ClassInputDemoViewModel.cs
@@ -0,0 +1,8 @@
+using CommunityToolkit.Mvvm.ComponentModel;
+
+namespace Ursa.Demo.ViewModels;
+
+public class ClassInputDemoViewModel: ObservableObject
+{
+
+}
\ No newline at end of file
diff --git a/demo/Ursa.Demo/ViewModels/MainViewViewModel.cs b/demo/Ursa.Demo/ViewModels/MainViewViewModel.cs
index 50ab100..752530f 100644
--- a/demo/Ursa.Demo/ViewModels/MainViewViewModel.cs
+++ b/demo/Ursa.Demo/ViewModels/MainViewViewModel.cs
@@ -28,6 +28,7 @@ public class MainViewViewModel : ViewModelBase
MenuKeys.MenuKeyBadge => new BadgeDemoViewModel(),
MenuKeys.MenuKeyBanner => new BannerDemoViewModel(),
MenuKeys.MenuKeyButtonGroup => new ButtonGroupDemoViewModel(),
+ MenuKeys.MenuKeyClassInput => new ClassInputDemoViewModel(),
MenuKeys.MenuKeyDialog => new DialogDemoViewModel(),
MenuKeys.MenuKeyDivider => new DividerDemoViewModel(),
MenuKeys.MenuKeyDualBadge => new DualBadgeDemoViewModel(),
diff --git a/demo/Ursa.Demo/ViewModels/MenuViewModel.cs b/demo/Ursa.Demo/ViewModels/MenuViewModel.cs
index 672c5b4..0f7c41e 100644
--- a/demo/Ursa.Demo/ViewModels/MenuViewModel.cs
+++ b/demo/Ursa.Demo/ViewModels/MenuViewModel.cs
@@ -15,6 +15,7 @@ public class MenuViewModel: ViewModelBase
new() { MenuHeader = "Badge", Key = MenuKeys.MenuKeyBadge },
new() { MenuHeader = "Banner", Key = MenuKeys.MenuKeyBanner },
new() { MenuHeader = "Button Group", Key = MenuKeys.MenuKeyButtonGroup, Status = "Updated"},
+ new() { MenuHeader = "Class Input", Key = MenuKeys.MenuKeyClassInput, Status = "New" },
new() { MenuHeader = "Dialog", Key = MenuKeys.MenuKeyDialog },
new() { MenuHeader = "Divider", Key = MenuKeys.MenuKeyDivider },
new() { MenuHeader = "DualBadge", Key = MenuKeys.MenuKeyDualBadge },
diff --git a/src/Ursa.Themes.Semi/Controls/ControlClassesInput.axaml b/src/Ursa.Themes.Semi/Controls/ControlClassesInput.axaml
new file mode 100644
index 0000000..7d68594
--- /dev/null
+++ b/src/Ursa.Themes.Semi/Controls/ControlClassesInput.axaml
@@ -0,0 +1,14 @@
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/Ursa.Themes.Semi/Controls/_index.axaml b/src/Ursa.Themes.Semi/Controls/_index.axaml
index f21a847..7c21320 100644
--- a/src/Ursa.Themes.Semi/Controls/_index.axaml
+++ b/src/Ursa.Themes.Semi/Controls/_index.axaml
@@ -4,6 +4,7 @@
+
diff --git a/src/Ursa/Controls/ControlClassesInput/ControlClassesInput.cs b/src/Ursa/Controls/ControlClassesInput/ControlClassesInput.cs
new file mode 100644
index 0000000..e1b444c
--- /dev/null
+++ b/src/Ursa/Controls/ControlClassesInput/ControlClassesInput.cs
@@ -0,0 +1,147 @@
+using System.Collections.ObjectModel;
+using System.Collections.Specialized;
+using Avalonia;
+using Avalonia.Collections;
+using Avalonia.Controls;
+using Avalonia.Controls.Primitives;
+
+namespace Ursa.Controls;
+
+public class ControlClassesInput: TemplatedControl
+{
+ LinkedList> _history = new();
+ LinkedList> _undoHistory = new();
+
+ public int CountOfHistoricalRecord { get; set; } = 10;
+
+ public static readonly StyledProperty TargetProperty = AvaloniaProperty.Register(
+ nameof(Target));
+
+ public Control? Target
+ {
+ get => GetValue(TargetProperty);
+ set => SetValue(TargetProperty, value);
+ }
+
+ public static readonly StyledProperty?> TargetClassesProperty = AvaloniaProperty.Register?>(
+ nameof(TargetClasses));
+
+ public ObservableCollection? TargetClasses
+ {
+ get => GetValue(TargetClassesProperty);
+ set => SetValue(TargetClassesProperty, value);
+ }
+
+ public static readonly AttachedProperty SourceProperty =
+ AvaloniaProperty.RegisterAttached("Source");
+
+ public static void SetSource(StyledElement obj, ControlClassesInput value) => obj.SetValue(SourceProperty, value);
+ public static ControlClassesInput? GetSource(StyledElement obj) => obj.GetValue(SourceProperty);
+
+ static ControlClassesInput()
+ {
+ TargetClassesProperty.Changed.AddClassHandler?>((o,e)=>o.OnClassesChanged(e));
+ SourceProperty.Changed.AddClassHandler(HandleSourceChange);
+ }
+
+ public ControlClassesInput()
+ {
+ TargetClasses = new ObservableCollection();
+ TargetClasses.CollectionChanged += InccOnCollectionChanged;
+ }
+
+ private List _targets = new();
+
+ private static void HandleSourceChange(StyledElement arg1, AvaloniaPropertyChangedEventArgs arg2)
+ {
+ var newControl = arg2.NewValue.Value;
+ if (newControl is null) return;
+ newControl._targets.Add(arg1);
+ var oldControl = arg2.OldValue.Value;
+ if (oldControl is not null)
+ {
+ newControl._targets.Remove(oldControl);
+ }
+ }
+
+ private static readonly char[] _separators = {' ', '\t', '\n', '\r'};
+
+ private void OnClassesChanged(AvaloniaPropertyChangedEventArgs?> args)
+ {
+ var newValue = args.NewValue.Value;
+ if (newValue is null)
+ {
+ SaveHistory(new List());
+ return;
+ }
+ else
+ {
+ var classes = newValue.Where(a => !string.IsNullOrWhiteSpace(a)).Distinct().ToList();
+ SaveHistory(classes);
+ if (newValue is INotifyCollectionChanged incc)
+ {
+ incc.CollectionChanged+=InccOnCollectionChanged;
+ }
+ return;
+ }
+ }
+
+ private void InccOnCollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
+ {
+ SaveHistory(TargetClasses?.ToList() ?? new List());
+ }
+
+ private void SaveHistory(List strings)
+ {
+ _history.AddLast(strings);
+ if (_history.Count > CountOfHistoricalRecord)
+ {
+ _history.RemoveFirst();
+ }
+ SetClassesToTarget();
+ }
+
+ private void SetClassesToTarget()
+ {
+ List strings;
+ if (_history.Count == 0)
+ {
+ strings = new List();
+ }
+ else
+ {
+ strings = _history.Last.Value;
+ }
+ if (Target is not null)
+ {
+ Target.Classes.Replace(strings);
+ }
+ foreach (var target in _targets)
+ {
+ target.Classes.Replace(strings);
+ }
+ }
+
+ public void UnDo()
+ {
+ var node = _history.Last;
+ _history.RemoveLast();
+ _undoHistory.AddFirst(node);
+ SetCurrentValue(TargetClassesProperty, new AvaloniaList(node.Value));
+ SetClassesToTarget();
+ }
+
+ public void Redo()
+ {
+ var node = _undoHistory.First;
+ _undoHistory.RemoveFirst();
+ _history.AddLast(node);
+ SetCurrentValue(TargetClassesProperty, new AvaloniaList(node.Value));
+ SetClassesToTarget();
+ }
+
+ public void Clear()
+ {
+ SaveHistory(new List());
+ }
+}
\ No newline at end of file