From da511c60780f84d17b991eee1846a9b9750a81e1 Mon Sep 17 00:00:00 2001 From: rabbitism Date: Thu, 8 Feb 2024 18:17:08 +0800 Subject: [PATCH 1/3] feat: add selection list. --- demo/Ursa.Demo/Models/MenuKeys.cs | 1 + demo/Ursa.Demo/Pages/SelectionBoxDemo.axaml | 15 +++ .../Ursa.Demo/Pages/SelectionBoxDemo.axaml.cs | 13 ++ .../Ursa.Demo/ViewModels/MainViewViewModel.cs | 1 + demo/Ursa.Demo/ViewModels/MenuViewModel.cs | 1 + .../ViewModels/SelectionListDemoViewModel.cs | 17 +++ .../Controls/SelectionList.axaml | 52 ++++++++ src/Ursa.Themes.Semi/Controls/_index.axaml | 1 + .../Controls/SelectionList/SelectionList.cs | 126 ++++++++++++++++++ .../SelectionList/SelectionListItem.cs | 38 ++++++ 10 files changed, 265 insertions(+) create mode 100644 demo/Ursa.Demo/Pages/SelectionBoxDemo.axaml create mode 100644 demo/Ursa.Demo/Pages/SelectionBoxDemo.axaml.cs create mode 100644 demo/Ursa.Demo/ViewModels/SelectionListDemoViewModel.cs create mode 100644 src/Ursa.Themes.Semi/Controls/SelectionList.axaml create mode 100644 src/Ursa/Controls/SelectionList/SelectionList.cs create mode 100644 src/Ursa/Controls/SelectionList/SelectionListItem.cs diff --git a/demo/Ursa.Demo/Models/MenuKeys.cs b/demo/Ursa.Demo/Models/MenuKeys.cs index 60d6ef2..ea8f8f5 100644 --- a/demo/Ursa.Demo/Models/MenuKeys.cs +++ b/demo/Ursa.Demo/Models/MenuKeys.cs @@ -22,6 +22,7 @@ public static class MenuKeys public const string MenuKeyNumericUpDown = "NumericUpDown"; public const string MenuKeyPagination = "Pagination"; public const string MenuKeyRangeSlider = "RangeSlider"; + public const string MenuKeySelectionList = "SelectionList"; public const string MenuKeyTagInput = "TagInput"; public const string MenuKeyTimeline = "Timeline"; public const string MenuKeyTwoTonePathIcon = "TwoTonePathIcon"; diff --git a/demo/Ursa.Demo/Pages/SelectionBoxDemo.axaml b/demo/Ursa.Demo/Pages/SelectionBoxDemo.axaml new file mode 100644 index 0000000..5ef2092 --- /dev/null +++ b/demo/Ursa.Demo/Pages/SelectionBoxDemo.axaml @@ -0,0 +1,15 @@ + + + + + diff --git a/demo/Ursa.Demo/Pages/SelectionBoxDemo.axaml.cs b/demo/Ursa.Demo/Pages/SelectionBoxDemo.axaml.cs new file mode 100644 index 0000000..60d5ccd --- /dev/null +++ b/demo/Ursa.Demo/Pages/SelectionBoxDemo.axaml.cs @@ -0,0 +1,13 @@ +using Avalonia; +using Avalonia.Controls; +using Avalonia.Markup.Xaml; + +namespace Ursa.Demo.Pages; + +public partial class SelectionListDemo : UserControl +{ + public SelectionListDemo() + { + InitializeComponent(); + } +} \ No newline at end of file diff --git a/demo/Ursa.Demo/ViewModels/MainViewViewModel.cs b/demo/Ursa.Demo/ViewModels/MainViewViewModel.cs index 6797172..0a20fe2 100644 --- a/demo/Ursa.Demo/ViewModels/MainViewViewModel.cs +++ b/demo/Ursa.Demo/ViewModels/MainViewViewModel.cs @@ -44,6 +44,7 @@ public class MainViewViewModel : ViewModelBase MenuKeys.MenuKeyNumericUpDown => new NumericUpDownDemoViewModel(), MenuKeys.MenuKeyPagination => new PaginationDemoViewModel(), MenuKeys.MenuKeyRangeSlider => new RangeSliderDemoViewModel(), + MenuKeys.MenuKeySelectionList => new SelectionListDemoViewModel(), MenuKeys.MenuKeyTagInput => new TagInputDemoViewModel(), MenuKeys.MenuKeyTimeline => new TimelineDemoViewModel(), MenuKeys.MenuKeyTwoTonePathIcon => new TwoTonePathIconDemoViewModel(), diff --git a/demo/Ursa.Demo/ViewModels/MenuViewModel.cs b/demo/Ursa.Demo/ViewModels/MenuViewModel.cs index cf807af..86c98fc 100644 --- a/demo/Ursa.Demo/ViewModels/MenuViewModel.cs +++ b/demo/Ursa.Demo/ViewModels/MenuViewModel.cs @@ -31,6 +31,7 @@ public class MenuViewModel: ViewModelBase new() { MenuHeader = "NumericUpDown", 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" }, new() { MenuHeader = "TagInput", Key = MenuKeys.MenuKeyTagInput }, new() { MenuHeader = "Theme Toggler", Key = MenuKeys.MenuKeyThemeToggler }, new() { MenuHeader = "Timeline", Key = MenuKeys.MenuKeyTimeline, Status = "Updated" }, diff --git a/demo/Ursa.Demo/ViewModels/SelectionListDemoViewModel.cs b/demo/Ursa.Demo/ViewModels/SelectionListDemoViewModel.cs new file mode 100644 index 0000000..c69a4bc --- /dev/null +++ b/demo/Ursa.Demo/ViewModels/SelectionListDemoViewModel.cs @@ -0,0 +1,17 @@ +using System.Collections.ObjectModel; +using CommunityToolkit.Mvvm.ComponentModel; + +namespace Ursa.Demo.ViewModels; + +public class SelectionListDemoViewModel: ObservableObject +{ + public ObservableCollection Items { get; set; } + + public SelectionListDemoViewModel() + { + Items = new ObservableCollection() + { + "Ding", "Otter", "Husky", "Mr. 17", "Cass" + }; + } +} \ No newline at end of file diff --git a/src/Ursa.Themes.Semi/Controls/SelectionList.axaml b/src/Ursa.Themes.Semi/Controls/SelectionList.axaml new file mode 100644 index 0000000..2123272 --- /dev/null +++ b/src/Ursa.Themes.Semi/Controls/SelectionList.axaml @@ -0,0 +1,52 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/Ursa.Themes.Semi/Controls/_index.axaml b/src/Ursa.Themes.Semi/Controls/_index.axaml index f770a28..4bd0c67 100644 --- a/src/Ursa.Themes.Semi/Controls/_index.axaml +++ b/src/Ursa.Themes.Semi/Controls/_index.axaml @@ -21,6 +21,7 @@ + diff --git a/src/Ursa/Controls/SelectionList/SelectionList.cs b/src/Ursa/Controls/SelectionList/SelectionList.cs new file mode 100644 index 0000000..e530179 --- /dev/null +++ b/src/Ursa/Controls/SelectionList/SelectionList.cs @@ -0,0 +1,126 @@ +using Avalonia; +using Avalonia.Controls; +using Avalonia.Controls.Metadata; +using Avalonia.Controls.Primitives; +using Avalonia.Controls.Selection; +using Avalonia.Controls.Templates; +using Avalonia.Input; +using Avalonia.Rendering.Composition; +using Avalonia.Rendering.Composition.Animations; +using Irihi.Avalonia.Shared.Helpers; + +namespace Ursa.Controls; + +[TemplatePart(PART_Indicator, typeof(Control))] +public class SelectionList: SelectingItemsControl +{ + public const string PART_Indicator = "PART_Indicator"; + private static readonly FuncTemplate DefaultPanel = new(() => new StackPanel()); + + private Control? _indicator; + private ImplicitAnimationCollection? _implicitAnimations; + + static SelectionList() + { + SelectionModeProperty.OverrideMetadata( + new StyledPropertyMetadata( + defaultValue: SelectionMode.Single, + coerce: (o, mode) => SelectionMode.Single) + ); + SelectedItemProperty.Changed.AddClassHandler((list, args) => + list.OnSelectedItemChanged(args)); + } + + private void OnSelectedItemChanged(AvaloniaPropertyChangedEventArgs args) + { + var newValue = args.NewValue.Value; + if (newValue is null) + { + OpacityProperty.SetValue(0d, _indicator); + return; + } + var container = ContainerFromItem(newValue); + if (container is null) + { + OpacityProperty.SetValue(0d, _indicator); + return; + } + OpacityProperty.SetValue(1d, _indicator); + InvalidateMeasure(); + InvalidateArrange(); + } + + protected override Size ArrangeOverride(Size finalSize) + { + var size = base.ArrangeOverride(finalSize); + if(_indicator is not null && SelectedItem is not null) + { + var container = ContainerFromItem(SelectedItem); + if (container is null) return size; + _indicator.Arrange(container.Bounds); + } + return size; + } + + 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 SelectionListItem(); + } + + protected override void OnApplyTemplate(TemplateAppliedEventArgs e) + { + base.OnApplyTemplate(e); + _indicator = e.NameScope.Find(PART_Indicator); + _indicator?.Arrange(new Rect()); + if (_indicator is not null) + { + _indicator.Opacity = 0; + SetUpAnimation(); + if (ElementComposition.GetElementVisual(_indicator) is { } v) + { + v.ImplicitAnimations = _implicitAnimations; + } + _indicator.SizeChanged += OnIndicatorSizeChanged; + } + } + + private void OnIndicatorSizeChanged(object sender, SizeChangedEventArgs e) + { + + } + + internal void SelectByIndex(int index) + { + using var operation = Selection.BatchUpdate(); + Selection.Clear(); + Selection.Select(index); + } + + private void SetUpAnimation() + { + var compositor = ElementComposition.GetElementVisual(this)!.Compositor; + var offsetAnimation = compositor.CreateVector3KeyFrameAnimation(); + offsetAnimation.Target = "Offset"; + offsetAnimation.InsertExpressionKeyFrame(1.0f, "this.FinalValue"); + offsetAnimation.Duration = TimeSpan.FromSeconds(0.3); + var sizeAnimation = compositor.CreateVector2KeyFrameAnimation(); + sizeAnimation.Target = "Size"; + sizeAnimation.InsertExpressionKeyFrame(1.0f, "this.FinalValue"); + sizeAnimation.Duration = TimeSpan.FromSeconds(0.3); + var opacityAnimation = compositor.CreateScalarKeyFrameAnimation(); + opacityAnimation.Target = nameof(CompositionVisual.Opacity); + opacityAnimation.InsertExpressionKeyFrame(1.0f, "this.FinalValue"); + opacityAnimation.Duration = TimeSpan.FromSeconds(0.3); + + _implicitAnimations = compositor.CreateImplicitAnimationCollection(); + _implicitAnimations["Offset"] = offsetAnimation; + _implicitAnimations["Size"] = sizeAnimation; + _implicitAnimations["Opacity"] = opacityAnimation; + } + +} \ No newline at end of file diff --git a/src/Ursa/Controls/SelectionList/SelectionListItem.cs b/src/Ursa/Controls/SelectionList/SelectionListItem.cs new file mode 100644 index 0000000..1e89704 --- /dev/null +++ b/src/Ursa/Controls/SelectionList/SelectionListItem.cs @@ -0,0 +1,38 @@ +using Avalonia; +using Avalonia.Controls; +using Avalonia.Controls.Mixins; +using Avalonia.Controls.Primitives; +using Avalonia.Input; + +namespace Ursa.Controls; + +public class SelectionListItem: ContentControl, ISelectable +{ + static SelectionListItem() + { + SelectableMixin.Attach(IsSelectedProperty); + PressedMixin.Attach(); + FocusableProperty.OverrideDefaultValue(true); + } + + private static readonly Point s_invalidPoint = new Point(double.NaN, double.NaN); + private Point _pointerDownPoint = s_invalidPoint; + + public static readonly StyledProperty IsSelectedProperty = SelectingItemsControl.IsSelectedProperty.AddOwner(); + + public bool IsSelected + { + get => GetValue(IsSelectedProperty); + set => SetValue(IsSelectedProperty, value); + } + + protected override void OnPointerPressed(PointerPressedEventArgs e) + { + base.OnPointerPressed(e); + if (ItemsControl.ItemsControlFromItemContaner(this) is SelectionList list) + { + int index = list.IndexFromContainer(this); + list.SelectByIndex(index); + } + } +} \ No newline at end of file From 2a08391dc76c89d51e1cb5056a9d08125e026bb0 Mon Sep 17 00:00:00 2001 From: rabbitism Date: Thu, 8 Feb 2024 19:19:38 +0800 Subject: [PATCH 2/3] feat: improve demo. --- demo/Ursa.Demo/Pages/SelectionBoxDemo.axaml | 64 +++++++++++++++---- .../ViewModels/SelectionListDemoViewModel.cs | 8 ++- .../Controls/SelectionList.axaml | 33 ++++++---- .../Controls/SelectionList/SelectionList.cs | 56 +++++++++++----- 4 files changed, 119 insertions(+), 42 deletions(-) diff --git a/demo/Ursa.Demo/Pages/SelectionBoxDemo.axaml b/demo/Ursa.Demo/Pages/SelectionBoxDemo.axaml index 5ef2092..18068cf 100644 --- a/demo/Ursa.Demo/Pages/SelectionBoxDemo.axaml +++ b/demo/Ursa.Demo/Pages/SelectionBoxDemo.axaml @@ -1,15 +1,55 @@ - + - + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/demo/Ursa.Demo/ViewModels/SelectionListDemoViewModel.cs b/demo/Ursa.Demo/ViewModels/SelectionListDemoViewModel.cs index c69a4bc..3711edd 100644 --- a/demo/Ursa.Demo/ViewModels/SelectionListDemoViewModel.cs +++ b/demo/Ursa.Demo/ViewModels/SelectionListDemoViewModel.cs @@ -3,9 +3,10 @@ using CommunityToolkit.Mvvm.ComponentModel; namespace Ursa.Demo.ViewModels; -public class SelectionListDemoViewModel: ObservableObject +public partial class SelectionListDemoViewModel: ObservableObject { public ObservableCollection Items { get; set; } + [ObservableProperty] private string? _selectedItem; public SelectionListDemoViewModel() { @@ -14,4 +15,9 @@ public class SelectionListDemoViewModel: ObservableObject "Ding", "Otter", "Husky", "Mr. 17", "Cass" }; } + + public void Clear() + { + SelectedItem = null; + } } \ No newline at end of file diff --git a/src/Ursa.Themes.Semi/Controls/SelectionList.axaml b/src/Ursa.Themes.Semi/Controls/SelectionList.axaml index 2123272..8306ddc 100644 --- a/src/Ursa.Themes.Semi/Controls/SelectionList.axaml +++ b/src/Ursa.Themes.Semi/Controls/SelectionList.axaml @@ -3,9 +3,19 @@ xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:u="https://irihi.tech/ursa"> - + + + + - + - + + + + FontWeight="{TemplateBinding FontWeight}" + ContentTemplate="{TemplateBinding ContentTemplate}" + Foreground="{TemplateBinding Foreground}" /> diff --git a/src/Ursa/Controls/SelectionList/SelectionList.cs b/src/Ursa/Controls/SelectionList/SelectionList.cs index e530179..240d809 100644 --- a/src/Ursa/Controls/SelectionList/SelectionList.cs +++ b/src/Ursa/Controls/SelectionList/SelectionList.cs @@ -1,6 +1,8 @@ +using System.Runtime.CompilerServices; using Avalonia; using Avalonia.Controls; using Avalonia.Controls.Metadata; +using Avalonia.Controls.Presenters; using Avalonia.Controls.Primitives; using Avalonia.Controls.Selection; using Avalonia.Controls.Templates; @@ -11,14 +13,23 @@ using Irihi.Avalonia.Shared.Helpers; namespace Ursa.Controls; -[TemplatePart(PART_Indicator, typeof(Control))] +[TemplatePart(PART_Indicator, typeof(ContentPresenter))] public class SelectionList: SelectingItemsControl { public const string PART_Indicator = "PART_Indicator"; private static readonly FuncTemplate DefaultPanel = new(() => new StackPanel()); - private Control? _indicator; private ImplicitAnimationCollection? _implicitAnimations; + private ContentPresenter? _indicator; + + public static readonly StyledProperty IndicatorProperty = AvaloniaProperty.Register( + nameof(Indicator)); + + public Control? Indicator + { + get => GetValue(IndicatorProperty); + set => SetValue(IndicatorProperty, value); + } static SelectionList() { @@ -75,25 +86,23 @@ public class SelectionList: SelectingItemsControl protected override void OnApplyTemplate(TemplateAppliedEventArgs e) { base.OnApplyTemplate(e); - _indicator = e.NameScope.Find(PART_Indicator); - _indicator?.Arrange(new Rect()); + _indicator= e.NameScope.Find(PART_Indicator); + EnsureIndicatorAnimation(); + } + + private void EnsureIndicatorAnimation() + { if (_indicator is not null) { _indicator.Opacity = 0; SetUpAnimation(); if (ElementComposition.GetElementVisual(_indicator) is { } v) { - v.ImplicitAnimations = _implicitAnimations; + v.ImplicitAnimations = _implicitAnimations; } - _indicator.SizeChanged += OnIndicatorSizeChanged; } } - private void OnIndicatorSizeChanged(object sender, SizeChangedEventArgs e) - { - - } - internal void SelectByIndex(int index) { using var operation = Selection.BatchUpdate(); @@ -103,13 +112,16 @@ public class SelectionList: SelectingItemsControl private void SetUpAnimation() { + if (_implicitAnimations != null) return; + var compositorVisual = ElementComposition.GetElementVisual(this); + if (compositorVisual is null) return; var compositor = ElementComposition.GetElementVisual(this)!.Compositor; var offsetAnimation = compositor.CreateVector3KeyFrameAnimation(); - offsetAnimation.Target = "Offset"; + offsetAnimation.Target = nameof(CompositionVisual.Offset); offsetAnimation.InsertExpressionKeyFrame(1.0f, "this.FinalValue"); offsetAnimation.Duration = TimeSpan.FromSeconds(0.3); var sizeAnimation = compositor.CreateVector2KeyFrameAnimation(); - sizeAnimation.Target = "Size"; + sizeAnimation.Target = nameof(CompositionVisual.Size); sizeAnimation.InsertExpressionKeyFrame(1.0f, "this.FinalValue"); sizeAnimation.Duration = TimeSpan.FromSeconds(0.3); var opacityAnimation = compositor.CreateScalarKeyFrameAnimation(); @@ -118,9 +130,19 @@ public class SelectionList: SelectingItemsControl opacityAnimation.Duration = TimeSpan.FromSeconds(0.3); _implicitAnimations = compositor.CreateImplicitAnimationCollection(); - _implicitAnimations["Offset"] = offsetAnimation; - _implicitAnimations["Size"] = sizeAnimation; - _implicitAnimations["Opacity"] = opacityAnimation; + _implicitAnimations[nameof(CompositionVisual.Offset)] = offsetAnimation; + _implicitAnimations[nameof(CompositionVisual.Size)] = sizeAnimation; + _implicitAnimations[nameof(CompositionVisual.Opacity)] = opacityAnimation; } -} \ No newline at end of file + protected override void OnKeyDown(KeyEventArgs e) + { + var hotkeys = Application.Current!.PlatformSettings?.HotkeyConfiguration; + + if (e.Key.ToNavigationDirection() is { } direction && direction.IsDirectional()) + { + e.Handled |= MoveSelection(direction, WrapSelection); + } + base.OnKeyDown(e); + } +} From c288836ce6d05b6aaf301dfa8cc025f4917b5a12 Mon Sep 17 00:00:00 2001 From: rabbitism Date: Thu, 8 Feb 2024 20:14:11 +0800 Subject: [PATCH 3/3] fix: hack a default size. --- demo/Ursa.Demo/ViewModels/MenuViewModel.cs | 2 +- src/Ursa/Controls/SelectionList/SelectionList.cs | 5 +++++ 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/demo/Ursa.Demo/ViewModels/MenuViewModel.cs b/demo/Ursa.Demo/ViewModels/MenuViewModel.cs index 86c98fc..1377af9 100644 --- a/demo/Ursa.Demo/ViewModels/MenuViewModel.cs +++ b/demo/Ursa.Demo/ViewModels/MenuViewModel.cs @@ -31,7 +31,7 @@ public class MenuViewModel: ViewModelBase new() { MenuHeader = "NumericUpDown", 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" }, + new() { MenuHeader = "Selection List", Key = MenuKeys.MenuKeySelectionList, Status = "New" }, new() { MenuHeader = "TagInput", Key = MenuKeys.MenuKeyTagInput }, new() { MenuHeader = "Theme Toggler", Key = MenuKeys.MenuKeyThemeToggler }, new() { MenuHeader = "Timeline", Key = MenuKeys.MenuKeyTimeline, Status = "Updated" }, diff --git a/src/Ursa/Controls/SelectionList/SelectionList.cs b/src/Ursa/Controls/SelectionList/SelectionList.cs index 240d809..ba102cc 100644 --- a/src/Ursa/Controls/SelectionList/SelectionList.cs +++ b/src/Ursa/Controls/SelectionList/SelectionList.cs @@ -70,6 +70,11 @@ public class SelectionList: SelectingItemsControl if (container is null) return size; _indicator.Arrange(container.Bounds); } + else + { + // This is a hack. The indicator is not visible, so we arrange it to a 1x1 rectangle + _indicator?.Arrange(new Rect(new Point(), new Size(1, 1))); + } return size; }