From 9d249b01cddb6a178bcce01447c96f8e89895452 Mon Sep 17 00:00:00 2001 From: rabbitism Date: Thu, 29 Jun 2023 01:45:58 +0800 Subject: [PATCH 1/7] feat: initialize tag input --- demo/Ursa.Demo/Pages/TagInputDemo.axaml | 12 ++ demo/Ursa.Demo/Pages/TagInputDemo.axaml.cs | 13 +++ demo/Ursa.Demo/Views/MainWindow.axaml | 3 + src/Ursa.Themes.Semi/Controls/TagInput.axaml | 87 +++++++++++++++ src/Ursa.Themes.Semi/Controls/_index.axaml | 1 + src/Ursa/AssemblyInfo.cs | 2 +- src/Ursa/Controls/TagInput/TagInput.cs | 76 +++++++++++++ src/Ursa/Controls/TagInput/TagInputPanel.cs | 109 +++++++++++++++++++ 8 files changed, 302 insertions(+), 1 deletion(-) create mode 100644 demo/Ursa.Demo/Pages/TagInputDemo.axaml create mode 100644 demo/Ursa.Demo/Pages/TagInputDemo.axaml.cs create mode 100644 src/Ursa.Themes.Semi/Controls/TagInput.axaml create mode 100644 src/Ursa/Controls/TagInput/TagInput.cs create mode 100644 src/Ursa/Controls/TagInput/TagInputPanel.cs diff --git a/demo/Ursa.Demo/Pages/TagInputDemo.axaml b/demo/Ursa.Demo/Pages/TagInputDemo.axaml new file mode 100644 index 0000000..4cc5c4a --- /dev/null +++ b/demo/Ursa.Demo/Pages/TagInputDemo.axaml @@ -0,0 +1,12 @@ + + + diff --git a/demo/Ursa.Demo/Pages/TagInputDemo.axaml.cs b/demo/Ursa.Demo/Pages/TagInputDemo.axaml.cs new file mode 100644 index 0000000..f19dc0a --- /dev/null +++ b/demo/Ursa.Demo/Pages/TagInputDemo.axaml.cs @@ -0,0 +1,13 @@ +using Avalonia; +using Avalonia.Controls; +using Avalonia.Markup.Xaml; + +namespace Ursa.Demo.Pages; + +public partial class TagInputDemo : UserControl +{ + public TagInputDemo() + { + InitializeComponent(); + } +} \ No newline at end of file diff --git a/demo/Ursa.Demo/Views/MainWindow.axaml b/demo/Ursa.Demo/Views/MainWindow.axaml index d6da845..1b6c162 100644 --- a/demo/Ursa.Demo/Views/MainWindow.axaml +++ b/demo/Ursa.Demo/Views/MainWindow.axaml @@ -47,6 +47,9 @@ + + + diff --git a/src/Ursa.Themes.Semi/Controls/TagInput.axaml b/src/Ursa.Themes.Semi/Controls/TagInput.axaml new file mode 100644 index 0000000..9539c85 --- /dev/null +++ b/src/Ursa.Themes.Semi/Controls/TagInput.axaml @@ -0,0 +1,87 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/Ursa.Themes.Semi/Controls/_index.axaml b/src/Ursa.Themes.Semi/Controls/_index.axaml index 7c621a3..bbf2752 100644 --- a/src/Ursa.Themes.Semi/Controls/_index.axaml +++ b/src/Ursa.Themes.Semi/Controls/_index.axaml @@ -9,6 +9,7 @@ + diff --git a/src/Ursa/AssemblyInfo.cs b/src/Ursa/AssemblyInfo.cs index 0b0375e..af243bf 100644 --- a/src/Ursa/AssemblyInfo.cs +++ b/src/Ursa/AssemblyInfo.cs @@ -1,5 +1,5 @@ using Avalonia.Metadata; +[assembly:XmlnsPrefix("https://irihi.tech/ursa", "u")] [assembly:XmlnsDefinition("https://irihi.tech/ursa", "Ursa")] [assembly:XmlnsDefinition("https://irihi.tech/ursa", "Ursa.Controls")] -[assembly:XmlnsPrefix("https://irihi.tech/ursa", "u")] \ No newline at end of file diff --git a/src/Ursa/Controls/TagInput/TagInput.cs b/src/Ursa/Controls/TagInput/TagInput.cs new file mode 100644 index 0000000..21bde20 --- /dev/null +++ b/src/Ursa/Controls/TagInput/TagInput.cs @@ -0,0 +1,76 @@ +using System.Collections; +using Avalonia; +using Avalonia.Collections; +using Avalonia.Controls; +using Avalonia.Controls.Primitives; +using Avalonia.Controls.Templates; +using Avalonia.Layout; +using Avalonia.Styling; + +namespace Ursa.Controls; + +public class TagInput: TemplatedControl +{ + public static readonly StyledProperty> TagsProperty = AvaloniaProperty.Register>( + nameof(Tags)); + + private TextBox _textBox; + + public IList Tags + { + get => GetValue(TagsProperty); + set => SetValue(TagsProperty, value); + } + + public static readonly StyledProperty ItemsProperty = AvaloniaProperty.Register( + nameof(Items)); + + public IList Items + { + get => GetValue(ItemsProperty); + set => SetValue(ItemsProperty, value); + } + + public TagInput() + { + _textBox = new TextBox(); + } + + public static readonly StyledProperty InputThemeProperty = AvaloniaProperty.Register( + nameof(InputTheme)); + + public ControlTheme InputTheme + { + get => GetValue(InputThemeProperty); + set => SetValue(InputThemeProperty, value); + } + + public static readonly StyledProperty ItemTemplateProperty = AvaloniaProperty.Register( + nameof(ItemTemplate)); + + public IDataTemplate? ItemTemplate + { + get => GetValue(ItemTemplateProperty); + set => SetValue(ItemTemplateProperty, value); + } + + protected override void OnApplyTemplate(TemplateAppliedEventArgs e) + { + base.OnApplyTemplate(e); + Items = new AvaloniaList(); + if (IsSet(InputThemeProperty) && InputTheme.TargetType == typeof(TextBox)) + { + _textBox.Theme = InputTheme; + } + _textBox.KeyDown += (sender, args) => + { + if (args.Key == Avalonia.Input.Key.Enter) + { + Items.Insert(Items.Count - 1, _textBox.Text); + // Tags.Insert(Items.Count - 1, _textBox.Text ?? string.Empty); + _textBox.Text = ""; + } + }; + Items.Add(_textBox); + } +} \ No newline at end of file diff --git a/src/Ursa/Controls/TagInput/TagInputPanel.cs b/src/Ursa/Controls/TagInput/TagInputPanel.cs new file mode 100644 index 0000000..afd19f4 --- /dev/null +++ b/src/Ursa/Controls/TagInput/TagInputPanel.cs @@ -0,0 +1,109 @@ +using Avalonia; +using Avalonia.Controls; +using Avalonia.Utilities; + +namespace Ursa.Controls; + +/// +/// TagInputPanel is a horizontal wrap with last item filling last row. +/// +public class TagInputPanel: Panel +{ + protected override Size MeasureOverride(Size availableSize) + { + // return base.MeasureOverride(availableSize); + double currentLineX = 0; + double currentLineHeight = 0; + double totalHeight = 0; + + var children = Children; + for (int i = 0; i < children.Count-1; i++) + { + var child = children[i]; + child.Measure(availableSize); + double deltaX = availableSize.Width - currentLineX; + // Width is enough to place next child + if (MathUtilities.GreaterThan(deltaX, child.DesiredSize.Width)) + { + currentLineX+=child.DesiredSize.Width; + currentLineHeight = Math.Max(currentLineHeight, child.DesiredSize.Height); + } + // Width is not enough to place next child + // reset currentLineX and currentLineHeight + // accumulate last line height to total height. + // Notice: last line height accumulation only happens when restarting a new line, so it needs to finally add one more time outside iteration. + else + { + currentLineX = child.DesiredSize.Width; + totalHeight += currentLineHeight; + currentLineHeight = child.DesiredSize.Height; + } + } + + var last = children[children.Count - 1]; + last.Measure(availableSize); + double lastDeltaX = availableSize.Width - currentLineX; + // If width is not enough, add a new line, and recalculate total height + if (lastDeltaX < 30) + { + totalHeight+=currentLineHeight; + totalHeight += last.DesiredSize.Height; + } + else + { + currentLineHeight = Math.Max(currentLineHeight, last.DesiredSize.Height); + totalHeight += currentLineHeight; + } + + return new Size(availableSize.Width, totalHeight); + } + + protected override Size ArrangeOverride(Size finalSize) + { + double currentLineX = 0; + double currentLineHeight = 0; + double totalHeight = 0; + var children = Children; + for (int i = 0; i < children.Count - 1; i++) + { + var child = children[i]; + double deltaX = finalSize.Width - currentLineX; + // Width is enough to place next child + if (MathUtilities.GreaterThan(deltaX, child.DesiredSize.Width)) + { + child.Arrange(new Rect(currentLineX, totalHeight, child.DesiredSize.Width, child.DesiredSize.Height)); + currentLineX += child.DesiredSize.Width; + currentLineHeight = Math.Max(currentLineHeight, child.DesiredSize.Height); + } + // Width is not enough to place next child + // reset currentLineX and currentLineHeight + // accumulate last line height to total height. + // Notice: last line height accumulation only happens when restarting a new line, so it needs to finally add one more time outside iteration. + else + { + totalHeight += currentLineHeight; + child.Arrange(new Rect(0, totalHeight, finalSize.Width, child.DesiredSize.Height)); + currentLineX = child.DesiredSize.Width; + currentLineHeight = child.DesiredSize.Height; + } + } + + var last = children[children.Count - 1]; + double lastDeltaX = finalSize.Width - currentLineX; + // If width is not enough, add a new line, and recalculate total height + if (lastDeltaX < 10) + { + totalHeight += currentLineHeight; + last.Arrange(new Rect(0, totalHeight, finalSize.Width, last.DesiredSize.Height)); + totalHeight += last.DesiredSize.Height; + } + else + { + last.Arrange(new Rect(currentLineX, totalHeight,lastDeltaX, last.DesiredSize.Height)); + currentLineHeight = Math.Max(currentLineHeight, last.DesiredSize.Height); + totalHeight += currentLineHeight; + } + + return new Size(finalSize.Width, totalHeight); + } +} \ No newline at end of file From 2d353ad9e068b687a5138c1c643bf4fe457eebcc Mon Sep 17 00:00:00 2001 From: rabbitism Date: Thu, 29 Jun 2023 02:25:31 +0800 Subject: [PATCH 2/7] feat: add closing feature. --- demo/Ursa.Demo/Pages/TagInputDemo.axaml | 5 +- src/Ursa.Themes.Semi/Controls/TagInput.axaml | 36 ++++++++++-- src/Ursa/Controls/TagInput/ClosableTag.cs | 46 +++++++++++++++ src/Ursa/Controls/TagInput/TagInput.cs | 60 +++++++++++++++----- src/Ursa/Controls/TagInput/TagInputPanel.cs | 2 +- 5 files changed, 129 insertions(+), 20 deletions(-) create mode 100644 src/Ursa/Controls/TagInput/ClosableTag.cs diff --git a/demo/Ursa.Demo/Pages/TagInputDemo.axaml b/demo/Ursa.Demo/Pages/TagInputDemo.axaml index 4cc5c4a..d73fcb3 100644 --- a/demo/Ursa.Demo/Pages/TagInputDemo.axaml +++ b/demo/Ursa.Demo/Pages/TagInputDemo.axaml @@ -8,5 +8,8 @@ d:DesignHeight="450" d:DesignWidth="800" mc:Ignorable="d"> - + + + + diff --git a/src/Ursa.Themes.Semi/Controls/TagInput.axaml b/src/Ursa.Themes.Semi/Controls/TagInput.axaml index 9539c85..4443f32 100644 --- a/src/Ursa.Themes.Semi/Controls/TagInput.axaml +++ b/src/Ursa.Themes.Semi/Controls/TagInput.axaml @@ -9,10 +9,7 @@ - @@ -24,6 +21,7 @@ CornerRadius="3"> + + + + + + + + + + + + + + + diff --git a/src/Ursa/Controls/TagInput/ClosableTag.cs b/src/Ursa/Controls/TagInput/ClosableTag.cs new file mode 100644 index 0000000..de609f2 --- /dev/null +++ b/src/Ursa/Controls/TagInput/ClosableTag.cs @@ -0,0 +1,46 @@ +using System.Windows.Input; +using Avalonia; +using Avalonia.Controls; +using Avalonia.Controls.Metadata; +using Avalonia.Controls.Primitives; +using Avalonia.Input; + +namespace Ursa.Controls; + +[TemplatePart(PART_CloseButton, typeof(PathIcon))] +public class ClosableTag: ContentControl +{ + public const string PART_CloseButton = "PART_CloseButton"; + private PathIcon? _icon; + public static readonly StyledProperty CommandProperty = AvaloniaProperty.Register( + nameof(Command)); + + public ICommand? Command + { + get => GetValue(CommandProperty); + set => SetValue(CommandProperty, value); + } + + protected override void OnApplyTemplate(TemplateAppliedEventArgs e) + { + base.OnApplyTemplate(e); + if (_icon != null) + { + _icon.PointerPressed -= OnPointerPressed; + } + _icon = e.NameScope.Find(PART_CloseButton); + if (_icon != null) + { + _icon.PointerPressed += OnPointerPressed; + } + + } + + private void OnPointerPressed(object? sender, PointerPressedEventArgs args) + { + if (Command != null && Command.CanExecute(null)) + { + Command.Execute(this); + } + } +} \ No newline at end of file diff --git a/src/Ursa/Controls/TagInput/TagInput.cs b/src/Ursa/Controls/TagInput/TagInput.cs index 21bde20..c2115ee 100644 --- a/src/Ursa/Controls/TagInput/TagInput.cs +++ b/src/Ursa/Controls/TagInput/TagInput.cs @@ -1,21 +1,29 @@ using System.Collections; +using System.Collections.ObjectModel; using Avalonia; using Avalonia.Collections; using Avalonia.Controls; +using Avalonia.Controls.Metadata; +using Avalonia.Controls.Presenters; using Avalonia.Controls.Primitives; using Avalonia.Controls.Templates; +using Avalonia.Input; using Avalonia.Layout; using Avalonia.Styling; namespace Ursa.Controls; +[TemplatePart (PART_ItemsControl, typeof (ItemsControl))] public class TagInput: TemplatedControl { + public const string PART_ItemsControl = "PART_ItemsControl"; + + private readonly TextBox _textBox; + private ItemsControl? _itemsControl; + + public static readonly StyledProperty> TagsProperty = AvaloniaProperty.Register>( nameof(Tags)); - - private TextBox _textBox; - public IList Tags { get => GetValue(TagsProperty); @@ -24,7 +32,6 @@ public class TagInput: TemplatedControl public static readonly StyledProperty ItemsProperty = AvaloniaProperty.Register( nameof(Items)); - public IList Items { get => GetValue(ItemsProperty); @@ -34,6 +41,9 @@ public class TagInput: TemplatedControl public TagInput() { _textBox = new TextBox(); + _textBox.KeyDown += OnTextBoxKeyDown; + Items = new AvaloniaList(); + Tags = new ObservableCollection(); } public static readonly StyledProperty InputThemeProperty = AvaloniaProperty.Register( @@ -57,20 +67,42 @@ public class TagInput: TemplatedControl protected override void OnApplyTemplate(TemplateAppliedEventArgs e) { base.OnApplyTemplate(e); - Items = new AvaloniaList(); + _itemsControl = e.NameScope.Find(PART_ItemsControl); if (IsSet(InputThemeProperty) && InputTheme.TargetType == typeof(TextBox)) { _textBox.Theme = InputTheme; } - _textBox.KeyDown += (sender, args) => - { - if (args.Key == Avalonia.Input.Key.Enter) - { - Items.Insert(Items.Count - 1, _textBox.Text); - // Tags.Insert(Items.Count - 1, _textBox.Text ?? string.Empty); - _textBox.Text = ""; - } - }; Items.Add(_textBox); } + + private void OnTextBoxKeyDown(object? sender, KeyEventArgs args) + { + if (args.Key == Avalonia.Input.Key.Enter) + { + if (_textBox.Text?.Length > 0) + { + Items.Insert(Items.Count - 1, _textBox.Text); + Tags.Insert(Items.Count - 2, _textBox.Text ?? string.Empty); + _textBox.Text = ""; + } + + } + } + + public void Close(object o) + { + if (o is ClosableTag t) + { + var presenter = t.Parent as ContentPresenter; + if (presenter != null) + { + int? index = _itemsControl?.IndexFromContainer(presenter); + if (index is >= 0 && index < Items.Count - 1) + { + Items.RemoveAt(index.Value); + Tags.RemoveAt(index.Value); + } + } + } + } } \ No newline at end of file diff --git a/src/Ursa/Controls/TagInput/TagInputPanel.cs b/src/Ursa/Controls/TagInput/TagInputPanel.cs index afd19f4..9efd6d4 100644 --- a/src/Ursa/Controls/TagInput/TagInputPanel.cs +++ b/src/Ursa/Controls/TagInput/TagInputPanel.cs @@ -82,7 +82,7 @@ public class TagInputPanel: Panel else { totalHeight += currentLineHeight; - child.Arrange(new Rect(0, totalHeight, finalSize.Width, child.DesiredSize.Height)); + child.Arrange(new Rect(0, totalHeight, Math.Min(child.DesiredSize.Width, finalSize.Width), child.DesiredSize.Height)); currentLineX = child.DesiredSize.Width; currentLineHeight = child.DesiredSize.Height; } From c9eb87cd37a28c4c26dc524d81728203bfdc0a01 Mon Sep 17 00:00:00 2001 From: rabbitism Date: Thu, 29 Jun 2023 02:45:58 +0800 Subject: [PATCH 3/7] feat: add back handling. --- src/Ursa/Controls/TagInput/TagInput.cs | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/src/Ursa/Controls/TagInput/TagInput.cs b/src/Ursa/Controls/TagInput/TagInput.cs index c2115ee..38d54a5 100644 --- a/src/Ursa/Controls/TagInput/TagInput.cs +++ b/src/Ursa/Controls/TagInput/TagInput.cs @@ -8,6 +8,7 @@ using Avalonia.Controls.Presenters; using Avalonia.Controls.Primitives; using Avalonia.Controls.Templates; using Avalonia.Input; +using Avalonia.Interactivity; using Avalonia.Layout; using Avalonia.Styling; @@ -41,7 +42,7 @@ public class TagInput: TemplatedControl public TagInput() { _textBox = new TextBox(); - _textBox.KeyDown += OnTextBoxKeyDown; + _textBox.AddHandler(InputElement.KeyDownEvent, OnTextBoxKeyDown, RoutingStrategies.Tunnel); Items = new AvaloniaList(); Tags = new ObservableCollection(); } @@ -77,7 +78,7 @@ public class TagInput: TemplatedControl private void OnTextBoxKeyDown(object? sender, KeyEventArgs args) { - if (args.Key == Avalonia.Input.Key.Enter) + if (args.Key == Key.Enter) { if (_textBox.Text?.Length > 0) { @@ -85,7 +86,14 @@ public class TagInput: TemplatedControl Tags.Insert(Items.Count - 2, _textBox.Text ?? string.Empty); _textBox.Text = ""; } - + } + else if (args.Key == Key.Delete || args.Key == Key.Back) + { + if (_textBox.Text?.Length == 0) + { + Items.RemoveAt(Items.Count - 2); + Tags.RemoveAt(Items.Count - 1); + } } } From c878e450479f10e1f827ce69cb4eaa42f336e1ba Mon Sep 17 00:00:00 2001 From: rabbitism Date: Thu, 29 Jun 2023 03:10:23 +0800 Subject: [PATCH 4/7] feat: add separator and duplicate support, try to vertical center textbox. --- demo/Ursa.Demo/Pages/TagInputDemo.axaml | 9 +++- src/Ursa.Themes.Semi/Controls/TagInput.axaml | 16 ++++--- src/Ursa/Controls/TagInput/TagInput.cs | 44 +++++++++++++++++++- src/Ursa/Controls/TagInput/TagInputPanel.cs | 6 ++- 4 files changed, 64 insertions(+), 11 deletions(-) diff --git a/demo/Ursa.Demo/Pages/TagInputDemo.axaml b/demo/Ursa.Demo/Pages/TagInputDemo.axaml index d73fcb3..d5751e5 100644 --- a/demo/Ursa.Demo/Pages/TagInputDemo.axaml +++ b/demo/Ursa.Demo/Pages/TagInputDemo.axaml @@ -9,7 +9,14 @@ d:DesignWidth="800" mc:Ignorable="d"> - + + diff --git a/src/Ursa.Themes.Semi/Controls/TagInput.axaml b/src/Ursa.Themes.Semi/Controls/TagInput.axaml index 4443f32..47cf6c6 100644 --- a/src/Ursa.Themes.Semi/Controls/TagInput.axaml +++ b/src/Ursa.Themes.Semi/Controls/TagInput.axaml @@ -6,6 +6,7 @@ + @@ -15,9 +16,10 @@ - - + + @@ -84,14 +86,15 @@ - + @@ -99,6 +102,7 @@ Name="{x:Static u:ClosableTag.PART_CloseButton}" Width="8" Height="8" + Margin="4,0" Background="Transparent" Data="M17.6568 19.7782C18.2426 20.3639 19.1924 20.3639 19.7782 19.7782C20.3639 19.1924 20.3639 18.2426 19.7782 17.6568L14.1213 12L19.7782 6.34313C20.3639 5.75734 20.3639 4.8076 19.7782 4.22181C19.1924 3.63602 18.2426 3.63602 17.6568 4.22181L12 9.87866L6.34313 4.22181C5.75734 3.63602 4.8076 3.63602 4.22181 4.22181C3.63602 4.8076 3.63602 5.75734 4.22181 6.34313L9.87866 12L4.22181 17.6568C3.63602 18.2426 3.63602 19.1924 4.22181 19.7782C4.8076 20.3639 5.75734 20.3639 6.34313 19.7782L12 14.1213L17.6568 19.7782Z" DockPanel.Dock="Right" /> diff --git a/src/Ursa/Controls/TagInput/TagInput.cs b/src/Ursa/Controls/TagInput/TagInput.cs index 38d54a5..ad568a9 100644 --- a/src/Ursa/Controls/TagInput/TagInput.cs +++ b/src/Ursa/Controls/TagInput/TagInput.cs @@ -65,6 +65,24 @@ public class TagInput: TemplatedControl set => SetValue(ItemTemplateProperty, value); } + public static readonly StyledProperty SeparatorProperty = AvaloniaProperty.Register( + nameof(Separator)); + + public string Separator + { + get => GetValue(SeparatorProperty); + set => SetValue(SeparatorProperty, value); + } + + public static readonly StyledProperty AllowDuplicatesProperty = AvaloniaProperty.Register( + nameof(AllowDuplicates), defaultValue: true); + + public bool AllowDuplicates + { + get => GetValue(AllowDuplicatesProperty); + set => SetValue(AllowDuplicatesProperty, value); + } + protected override void OnApplyTemplate(TemplateAppliedEventArgs e) { base.OnApplyTemplate(e); @@ -82,8 +100,26 @@ public class TagInput: TemplatedControl { if (_textBox.Text?.Length > 0) { - Items.Insert(Items.Count - 1, _textBox.Text); - Tags.Insert(Items.Count - 2, _textBox.Text ?? string.Empty); + string[] values; + if (!string.IsNullOrEmpty(Separator)) + { + values = _textBox.Text.Split(new string[] { Separator }, + StringSplitOptions.RemoveEmptyEntries); + } + else + { + values = new[] { _textBox.Text }; + } + + if (!AllowDuplicates) + { + values = values.Distinct().Except(Tags).ToArray(); + } + for(int i = 0; i < values.Length; i++) + { + Items.Insert(Items.Count - 1, values[i]); + Tags.Insert(Items.Count - 2, values[i]); + } _textBox.Text = ""; } } @@ -91,6 +127,10 @@ public class TagInput: TemplatedControl { if (_textBox.Text?.Length == 0) { + if (Tags.Count == 0) + { + return; + } Items.RemoveAt(Items.Count - 2); Tags.RemoveAt(Items.Count - 1); } diff --git a/src/Ursa/Controls/TagInput/TagInputPanel.cs b/src/Ursa/Controls/TagInput/TagInputPanel.cs index 9efd6d4..38117e4 100644 --- a/src/Ursa/Controls/TagInput/TagInputPanel.cs +++ b/src/Ursa/Controls/TagInput/TagInputPanel.cs @@ -71,7 +71,7 @@ public class TagInputPanel: Panel // Width is enough to place next child if (MathUtilities.GreaterThan(deltaX, child.DesiredSize.Width)) { - child.Arrange(new Rect(currentLineX, totalHeight, child.DesiredSize.Width, child.DesiredSize.Height)); + child.Arrange(new Rect(currentLineX, totalHeight, child.DesiredSize.Width, Math.Max(child.DesiredSize.Height, currentLineHeight))); currentLineX += child.DesiredSize.Width; currentLineHeight = Math.Max(currentLineHeight, child.DesiredSize.Height); } @@ -99,7 +99,9 @@ public class TagInputPanel: Panel } else { - last.Arrange(new Rect(currentLineX, totalHeight,lastDeltaX, last.DesiredSize.Height)); + currentLineHeight = children.Count == 1 ? finalSize.Height : currentLineHeight; + last.Arrange(new Rect(currentLineX, totalHeight, lastDeltaX, + Math.Max(currentLineHeight, last.DesiredSize.Height))); currentLineHeight = Math.Max(currentLineHeight, last.DesiredSize.Height); totalHeight += currentLineHeight; } From 69e2ce99805e645eb79333c9b85d4b6a31342cf3 Mon Sep 17 00:00:00 2001 From: rabbitism Date: Thu, 29 Jun 2023 03:28:04 +0800 Subject: [PATCH 5/7] feat: remove item template dependency --- src/Ursa.Themes.Semi/Controls/TagInput.axaml | 6 +++--- src/Ursa/Controls/TagInput/TagInput.cs | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/Ursa.Themes.Semi/Controls/TagInput.axaml b/src/Ursa.Themes.Semi/Controls/TagInput.axaml index 47cf6c6..7a43ce9 100644 --- a/src/Ursa.Themes.Semi/Controls/TagInput.axaml +++ b/src/Ursa.Themes.Semi/Controls/TagInput.axaml @@ -17,8 +17,8 @@ @@ -55,7 +55,7 @@ - + Date: Thu, 29 Jun 2023 10:33:13 +0800 Subject: [PATCH 6/7] feat: change Items to readonly. --- src/Ursa/Controls/TagInput/TagInput.cs | 46 ++++++++++++++++++++------ 1 file changed, 35 insertions(+), 11 deletions(-) diff --git a/src/Ursa/Controls/TagInput/TagInput.cs b/src/Ursa/Controls/TagInput/TagInput.cs index fb64d1a..32a3e64 100644 --- a/src/Ursa/Controls/TagInput/TagInput.cs +++ b/src/Ursa/Controls/TagInput/TagInput.cs @@ -30,13 +30,14 @@ public class TagInput: TemplatedControl get => GetValue(TagsProperty); set => SetValue(TagsProperty, value); } - - public static readonly StyledProperty ItemsProperty = AvaloniaProperty.Register( - nameof(Items)); + + public static readonly DirectProperty ItemsProperty = AvaloniaProperty.RegisterDirect( + nameof(Items), o => o.Items); + private IList _items; public IList Items { - get => GetValue(ItemsProperty); - set => SetValue(ItemsProperty, value); + get => _items; + private set => SetAndRaise(ItemsProperty, ref _items, value); } public TagInput() @@ -83,17 +84,41 @@ public class TagInput: TemplatedControl set => SetValue(AllowDuplicatesProperty, value); } + static TagInput() + { + InputThemeProperty.Changed.AddClassHandler((o, e) => o.OnInputThemePropertyChanged(e)); + TagsProperty.Changed.AddClassHandler((o, e) => o.OnTagsPropertyChanged(e)); + } + protected override void OnApplyTemplate(TemplateAppliedEventArgs e) { base.OnApplyTemplate(e); _itemsControl = e.NameScope.Find(PART_ItemsControl); - if (IsSet(InputThemeProperty) && InputTheme.TargetType == typeof(TextBox)) - { - _textBox.Theme = InputTheme; - } Items.Add(_textBox); } + private void OnInputThemePropertyChanged(AvaloniaPropertyChangedEventArgs args) + { + var newTheme = args.GetNewValue(); + if (newTheme.TargetType == typeof(TextBox)) + { + _textBox.Theme = newTheme; + } + } + + private void OnTagsPropertyChanged(AvaloniaPropertyChangedEventArgs args) + { + var newTags = args.GetNewValue>(); + for (int i = 0; i < Items.Count - 1; i++) + { + Items.RemoveAt(Items.Count-1); + } + for (int i = 0; i < newTags.Count; i++) + { + Items.Insert(Items.Count - 1, newTags[i]); + } + } + private void OnTextBoxKeyDown(object? sender, KeyEventArgs args) { if (args.Key == Key.Enter) @@ -141,8 +166,7 @@ public class TagInput: TemplatedControl { if (o is Control t) { - var presenter = t.Parent as ContentPresenter; - if (presenter != null) + if (t.Parent is ContentPresenter presenter) { int? index = _itemsControl?.IndexFromContainer(presenter); if (index is >= 0 && index < Items.Count - 1) From 5911ac04fb7753ca8bc8ebc68e0ff6b874cb7727 Mon Sep 17 00:00:00 2001 From: rabbitism Date: Thu, 29 Jun 2023 11:12:56 +0800 Subject: [PATCH 7/7] feat: update styles, simplify index calculation. --- src/Ursa.Themes.Semi/Controls/TagInput.axaml | 30 ++++++-- .../Themes/Dark/TagInput.axaml | 4 + src/Ursa.Themes.Semi/Themes/Dark/_index.axaml | 1 + .../Themes/Light/TagInput.axaml | 4 + .../Themes/Light/_index.axaml | 1 + .../Themes/Shared/TagInput.axaml | 5 ++ .../Themes/Shared/_index.axaml | 1 + src/Ursa/Controls/TagInput/TagInput.cs | 77 +++++++++++++------ 8 files changed, 94 insertions(+), 29 deletions(-) create mode 100644 src/Ursa.Themes.Semi/Themes/Dark/TagInput.axaml create mode 100644 src/Ursa.Themes.Semi/Themes/Light/TagInput.axaml create mode 100644 src/Ursa.Themes.Semi/Themes/Shared/TagInput.axaml diff --git a/src/Ursa.Themes.Semi/Controls/TagInput.axaml b/src/Ursa.Themes.Semi/Controls/TagInput.axaml index 7a43ce9..6829870 100644 --- a/src/Ursa.Themes.Semi/Controls/TagInput.axaml +++ b/src/Ursa.Themes.Semi/Controls/TagInput.axaml @@ -6,7 +6,7 @@ - + @@ -16,10 +16,13 @@ + + @@ -48,6 +57,7 @@ + @@ -87,15 +97,19 @@ + + + + + Data="{DynamicResource ClosableTagCloseIconGlyph}" + DockPanel.Dock="Right" + Foreground="{TemplateBinding Foreground}" /> diff --git a/src/Ursa.Themes.Semi/Themes/Dark/TagInput.axaml b/src/Ursa.Themes.Semi/Themes/Dark/TagInput.axaml new file mode 100644 index 0000000..c32a707 --- /dev/null +++ b/src/Ursa.Themes.Semi/Themes/Dark/TagInput.axaml @@ -0,0 +1,4 @@ + + + + diff --git a/src/Ursa.Themes.Semi/Themes/Dark/_index.axaml b/src/Ursa.Themes.Semi/Themes/Dark/_index.axaml index c892d42..c9f2bea 100644 --- a/src/Ursa.Themes.Semi/Themes/Dark/_index.axaml +++ b/src/Ursa.Themes.Semi/Themes/Dark/_index.axaml @@ -9,6 +9,7 @@ + diff --git a/src/Ursa.Themes.Semi/Themes/Light/TagInput.axaml b/src/Ursa.Themes.Semi/Themes/Light/TagInput.axaml new file mode 100644 index 0000000..c3b76e1 --- /dev/null +++ b/src/Ursa.Themes.Semi/Themes/Light/TagInput.axaml @@ -0,0 +1,4 @@ + + + + diff --git a/src/Ursa.Themes.Semi/Themes/Light/_index.axaml b/src/Ursa.Themes.Semi/Themes/Light/_index.axaml index c892d42..c9f2bea 100644 --- a/src/Ursa.Themes.Semi/Themes/Light/_index.axaml +++ b/src/Ursa.Themes.Semi/Themes/Light/_index.axaml @@ -9,6 +9,7 @@ + diff --git a/src/Ursa.Themes.Semi/Themes/Shared/TagInput.axaml b/src/Ursa.Themes.Semi/Themes/Shared/TagInput.axaml new file mode 100644 index 0000000..2f3d2b7 --- /dev/null +++ b/src/Ursa.Themes.Semi/Themes/Shared/TagInput.axaml @@ -0,0 +1,5 @@ + + + M17.6568 19.7782C18.2426 20.3639 19.1924 20.3639 19.7782 19.7782C20.3639 19.1924 20.3639 18.2426 19.7782 17.6568L14.1213 12L19.7782 6.34313C20.3639 5.75734 20.3639 4.8076 19.7782 4.22181C19.1924 3.63602 18.2426 3.63602 17.6568 4.22181L12 9.87866L6.34313 4.22181C5.75734 3.63602 4.8076 3.63602 4.22181 4.22181C3.63602 4.8076 3.63602 5.75734 4.22181 6.34313L9.87866 12L4.22181 17.6568C3.63602 18.2426 3.63602 19.1924 4.22181 19.7782C4.8076 20.3639 5.75734 20.3639 6.34313 19.7782L12 14.1213L17.6568 19.7782Z + 32 + diff --git a/src/Ursa.Themes.Semi/Themes/Shared/_index.axaml b/src/Ursa.Themes.Semi/Themes/Shared/_index.axaml index 5e6a238..777a0a4 100644 --- a/src/Ursa.Themes.Semi/Themes/Shared/_index.axaml +++ b/src/Ursa.Themes.Semi/Themes/Shared/_index.axaml @@ -8,5 +8,6 @@ + diff --git a/src/Ursa/Controls/TagInput/TagInput.cs b/src/Ursa/Controls/TagInput/TagInput.cs index 32a3e64..5ee089b 100644 --- a/src/Ursa/Controls/TagInput/TagInput.cs +++ b/src/Ursa/Controls/TagInput/TagInput.cs @@ -14,26 +14,31 @@ using Avalonia.Styling; namespace Ursa.Controls; -[TemplatePart (PART_ItemsControl, typeof (ItemsControl))] -public class TagInput: TemplatedControl +[TemplatePart(PART_ItemsControl, typeof(ItemsControl))] +public class TagInput : TemplatedControl { public const string PART_ItemsControl = "PART_ItemsControl"; - + private readonly TextBox _textBox; private ItemsControl? _itemsControl; - - - public static readonly StyledProperty> TagsProperty = AvaloniaProperty.Register>( - nameof(Tags)); + + + public static readonly StyledProperty> TagsProperty = + AvaloniaProperty.Register>( + nameof(Tags)); + public IList Tags { get => GetValue(TagsProperty); set => SetValue(TagsProperty, value); } - - public static readonly DirectProperty ItemsProperty = AvaloniaProperty.RegisterDirect( - nameof(Items), o => o.Items); + + public static readonly DirectProperty ItemsProperty = + AvaloniaProperty.RegisterDirect( + nameof(Items), o => o.Items); + private IList _items; + public IList Items { get => _items; @@ -48,8 +53,9 @@ public class TagInput: TemplatedControl Tags = new ObservableCollection(); } - public static readonly StyledProperty InputThemeProperty = AvaloniaProperty.Register( - nameof(InputTheme)); + public static readonly StyledProperty InputThemeProperty = + AvaloniaProperty.Register( + nameof(InputTheme)); public ControlTheme InputTheme { @@ -57,8 +63,9 @@ public class TagInput: TemplatedControl set => SetValue(InputThemeProperty, value); } - public static readonly StyledProperty ItemTemplateProperty = AvaloniaProperty.Register( - nameof(ItemTemplate)); + public static readonly StyledProperty ItemTemplateProperty = + AvaloniaProperty.Register( + nameof(ItemTemplate)); public IDataTemplate? ItemTemplate { @@ -84,6 +91,27 @@ public class TagInput: TemplatedControl set => SetValue(AllowDuplicatesProperty, value); } + public static readonly StyledProperty InnerLeftContentProperty = + AvaloniaProperty.Register( + nameof(InnerLeftContent)); + + public object? InnerLeftContent + { + get => GetValue(InnerLeftContentProperty); + set => SetValue(InnerLeftContentProperty, value); + } + + public static readonly StyledProperty InnerRightContentProperty = + AvaloniaProperty.Register( + nameof(InnerRightContent)); + + public object? InnerRightContent + { + get => GetValue(InnerRightContentProperty); + set => SetValue(InnerRightContentProperty, value); + } + + static TagInput() { InputThemeProperty.Changed.AddClassHandler((o, e) => o.OnInputThemePropertyChanged(e)); @@ -93,7 +121,7 @@ public class TagInput: TemplatedControl protected override void OnApplyTemplate(TemplateAppliedEventArgs e) { base.OnApplyTemplate(e); - _itemsControl = e.NameScope.Find(PART_ItemsControl); + _itemsControl = e.NameScope.Find(PART_ItemsControl); Items.Add(_textBox); } @@ -105,14 +133,15 @@ public class TagInput: TemplatedControl _textBox.Theme = newTheme; } } - + private void OnTagsPropertyChanged(AvaloniaPropertyChangedEventArgs args) { var newTags = args.GetNewValue>(); for (int i = 0; i < Items.Count - 1; i++) { - Items.RemoveAt(Items.Count-1); + Items.RemoveAt(Items.Count - 1); } + for (int i = 0; i < newTags.Count; i++) { Items.Insert(Items.Count - 1, newTags[i]); @@ -140,11 +169,14 @@ public class TagInput: TemplatedControl { values = values.Distinct().Except(Tags).ToArray(); } - for(int i = 0; i < values.Length; i++) + + for (int i = 0; i < values.Length; i++) { - Items.Insert(Items.Count - 1, values[i]); - Tags.Insert(Items.Count - 2, values[i]); + int index = Items.Count - 1; + Items.Insert(index, values[i]); + Tags.Insert(index, values[i]); } + _textBox.Text = ""; } } @@ -156,8 +188,9 @@ public class TagInput: TemplatedControl { return; } - Items.RemoveAt(Items.Count - 2); - Tags.RemoveAt(Items.Count - 1); + int index = Items.Count - 2; + Items.RemoveAt(index); + Tags.RemoveAt(index); } } }