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