diff --git a/demo/Ursa.Demo/Pages/AnchorDemo.axaml b/demo/Ursa.Demo/Pages/AnchorDemo.axaml new file mode 100644 index 0000000..fce02d2 --- /dev/null +++ b/demo/Ursa.Demo/Pages/AnchorDemo.axaml @@ -0,0 +1,69 @@ + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/demo/Ursa.Demo/Pages/AnchorDemo.axaml.cs b/demo/Ursa.Demo/Pages/AnchorDemo.axaml.cs new file mode 100644 index 0000000..584f821 --- /dev/null +++ b/demo/Ursa.Demo/Pages/AnchorDemo.axaml.cs @@ -0,0 +1,13 @@ +using Avalonia; +using Avalonia.Controls; +using Avalonia.Markup.Xaml; + +namespace Ursa.Demo.Pages; + +public partial class AnchorDemo : UserControl +{ + public AnchorDemo() + { + InitializeComponent(); + } +} \ No newline at end of file diff --git a/demo/Ursa.Demo/ViewModels/AnchorDemoViewModel.cs b/demo/Ursa.Demo/ViewModels/AnchorDemoViewModel.cs new file mode 100644 index 0000000..37ca6fa --- /dev/null +++ b/demo/Ursa.Demo/ViewModels/AnchorDemoViewModel.cs @@ -0,0 +1,8 @@ +using CommunityToolkit.Mvvm.ComponentModel; + +namespace Ursa.Demo.ViewModels; + +public partial class AnchorDemoViewModel: ObservableObject +{ + +} \ No newline at end of file diff --git a/demo/Ursa.Demo/ViewModels/MainViewViewModel.cs b/demo/Ursa.Demo/ViewModels/MainViewViewModel.cs index 361b236..5f1ba3e 100644 --- a/demo/Ursa.Demo/ViewModels/MainViewViewModel.cs +++ b/demo/Ursa.Demo/ViewModels/MainViewViewModel.cs @@ -86,8 +86,9 @@ public partial class MainViewViewModel : ViewModelBase MenuKeys.MenuKeyToolBar => new ToolBarDemoViewModel(), MenuKeys.MenuKeyTreeComboBox => new TreeComboBoxDemoViewModel(), MenuKeys.MenuKeyTwoTonePathIcon => new TwoTonePathIconDemoViewModel(), - MenuKeys.AspectRatioLayout => new AspectRatioLayoutDemoViewModel(), - MenuKeys.PathPicker => new PathPickerDemoViewModel(), + MenuKeys.MenuKeyAspectRatioLayout => new AspectRatioLayoutDemoViewModel(), + MenuKeys.MenuKeyPathPicker => new PathPickerDemoViewModel(), + MenuKeys.MenuKeyAnchor => new AnchorDemoViewModel(), _ => throw new ArgumentOutOfRangeException(nameof(s), s, null) }; } diff --git a/demo/Ursa.Demo/ViewModels/MenuViewModel.cs b/demo/Ursa.Demo/ViewModels/MenuViewModel.cs index 14f4e60..297822b 100644 --- a/demo/Ursa.Demo/ViewModels/MenuViewModel.cs +++ b/demo/Ursa.Demo/ViewModels/MenuViewModel.cs @@ -26,7 +26,7 @@ public class MenuViewModel : ViewModelBase new() { MenuHeader = "MultiComboBox", Key = MenuKeys.MenuKeyMultiComboBox }, new() { MenuHeader = "Numeric UpDown", Key = MenuKeys.MenuKeyNumericUpDown }, new() { MenuHeader = "NumPad", Key = MenuKeys.MenuKeyNumPad }, - new() { MenuHeader = "PathPicker", Key = MenuKeys.PathPicker, Status = "New" }, + new() { MenuHeader = "PathPicker", Key = MenuKeys.MenuKeyPathPicker, Status = "New" }, new() { MenuHeader = "PinCode", Key = MenuKeys.MenuKeyPinCode }, new() { MenuHeader = "RangeSlider", Key = MenuKeys.MenuKeyRangeSlider }, new() { MenuHeader = "Rating", Key = MenuKeys.MenuKeyRating }, @@ -67,6 +67,7 @@ public class MenuViewModel : ViewModelBase { MenuHeader = "Navigation & Menus", Children = new ObservableCollection { + new() { MenuHeader = "Anchor", Key = MenuKeys.MenuKeyAnchor, Status = "New" }, new() { MenuHeader = "Breadcrumb", Key = MenuKeys.MenuKeyBreadcrumb, Status = "Updated" }, new() { MenuHeader = "Nav Menu", Key = MenuKeys.MenuKeyNavMenu, Status = "Updated" }, new() { MenuHeader = "Pagination", Key = MenuKeys.MenuKeyPagination }, @@ -78,7 +79,7 @@ public class MenuViewModel : ViewModelBase MenuHeader = "Layout & Display", Children = new ObservableCollection { - new() { MenuHeader = "AspectRatioLayout", Key = MenuKeys.AspectRatioLayout }, + new() { MenuHeader = "AspectRatioLayout", Key = MenuKeys.MenuKeyAspectRatioLayout }, new() { MenuHeader = "Avatar", Key = MenuKeys.MenuKeyAvatar, Status = "WIP" }, new() { MenuHeader = "Badge", Key = MenuKeys.MenuKeyBadge }, new() { MenuHeader = "Banner", Key = MenuKeys.MenuKeyBanner, Status = "Updated" }, @@ -154,6 +155,7 @@ public static class MenuKeys public const string MenuKeyToolBar = "ToolBar"; public const string MenuKeyTreeComboBox = "TreeComboBox"; public const string MenuKeyTwoTonePathIcon = "TwoTonePathIcon"; - public const string AspectRatioLayout = "AspectRatioLayout"; - public const string PathPicker = "PathPicker"; + public const string MenuKeyAspectRatioLayout = "AspectRatioLayout"; + public const string MenuKeyPathPicker = "PathPicker"; + public const string MenuKeyAnchor = "Anchor"; } \ No newline at end of file diff --git a/src/Ursa.Themes.Semi/Controls/Anchor.axaml b/src/Ursa.Themes.Semi/Controls/Anchor.axaml new file mode 100644 index 0000000..5422215 --- /dev/null +++ b/src/Ursa.Themes.Semi/Controls/Anchor.axaml @@ -0,0 +1,15 @@ + + + + + + + + + diff --git a/src/Ursa.Themes.Semi/Controls/_index.axaml b/src/Ursa.Themes.Semi/Controls/_index.axaml index ff8b078..b2672a9 100644 --- a/src/Ursa.Themes.Semi/Controls/_index.axaml +++ b/src/Ursa.Themes.Semi/Controls/_index.axaml @@ -1,5 +1,6 @@ + diff --git a/src/Ursa/Controls/Anchor/Anchor.cs b/src/Ursa/Controls/Anchor/Anchor.cs new file mode 100644 index 0000000..7c582b8 --- /dev/null +++ b/src/Ursa/Controls/Anchor/Anchor.cs @@ -0,0 +1,99 @@ +using Avalonia; +using Avalonia.Animation; +using Avalonia.Animation.Easings; +using Avalonia.Controls; +using Avalonia.Controls.Primitives; +using Avalonia.Styling; + +namespace Ursa.Controls; + +public class Anchor: SelectingItemsControl +{ + public static readonly StyledProperty TargetContainerProperty = AvaloniaProperty.Register( + nameof(TargetContainer)); + + public ScrollViewer? TargetContainer + { + get => GetValue(TargetContainerProperty); + set => SetValue(TargetContainerProperty, value); + } + + public static readonly AttachedProperty AnchorIdProperty = + AvaloniaProperty.RegisterAttached("AnchorId"); + + public static void SetAnchorId(Control obj, string value) => obj.SetValue(AnchorIdProperty, value); + public static string GetAnchorId(Control obj) => obj.GetValue(AnchorIdProperty); + + + 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) + { + var i = new AnchorItem(); + return i; + } + + internal void ScrollToAnchor(string anchorId) + { + if (TargetContainer is null) + return; + + var target = TargetContainer.FindControl(anchorId); + if (target is null) + return; + + var targetPosition = target.TranslatePoint(new Point(0, 0), TargetContainer); + if (targetPosition.HasValue) + { + TargetContainer.Offset = new Vector(0, targetPosition.Value.Y); + } + + } + + internal void ScrollToAnchor(Control target) + { + if (TargetContainer is null) + return; + + var targetPosition = target.TranslatePoint(new Point(0, 0), TargetContainer); + if (targetPosition.HasValue) + { + var from = TargetContainer.Offset.Y; + var to = TargetContainer.Offset.Y + targetPosition.Value.Y; + if(to > TargetContainer.Extent.Height - TargetContainer.Bounds.Height) + { + to = TargetContainer.Extent.Height - TargetContainer.Bounds.Height; + } + Animation animation = new Animation() + { + Duration = TimeSpan.FromSeconds(0.3), + Easing = new QuadraticEaseOut(), + Children = + { + new KeyFrame(){ + Setters = + { + new Setter(ScrollViewer.OffsetProperty, new Vector(0, from)), + }, + Cue = new Cue(0.0) + }, + new KeyFrame() + { + Setters = + { + new Setter(ScrollViewer.OffsetProperty, new Vector(0, to)) + }, + Cue = new Cue(1.0) + } + + } + }; + animation.RunAsync(TargetContainer); + // TargetContainer.Offset = TargetContainer.Offset.WithY(TargetContainer.Offset.Y + targetPosition.Value.Y); + } + } + +} \ No newline at end of file diff --git a/src/Ursa/Controls/Anchor/AnchorItem.cs b/src/Ursa/Controls/Anchor/AnchorItem.cs new file mode 100644 index 0000000..ae15b15 --- /dev/null +++ b/src/Ursa/Controls/Anchor/AnchorItem.cs @@ -0,0 +1,47 @@ +using System.Windows.Input; +using Avalonia; +using Avalonia.Controls; +using Avalonia.Input; +using Avalonia.VisualTree; + +namespace Ursa.Controls; + +public class AnchorItem: ContentControl +{ + public static readonly StyledProperty TargetProperty = AvaloniaProperty.Register( + nameof(Target)); + + public Control? Target + { + get => GetValue(TargetProperty); + set => SetValue(TargetProperty, value); + } + + public static readonly StyledProperty AnchorNameProperty = AvaloniaProperty.Register( + nameof(AnchorName)); + public string? AnchorName + { + get => GetValue(AnchorNameProperty); + set => SetValue(AnchorNameProperty, value); + } + + private Anchor? _root; + + protected override void OnAttachedToVisualTree(VisualTreeAttachmentEventArgs e) + { + base.OnAttachedToVisualTree(e); + _root = this.FindAncestorOfType() ?? + throw new InvalidOperationException("AnchorItem must be inside an Anchor control."); + } + + protected override void OnPointerPressed(PointerPressedEventArgs e) + { + base.OnPointerPressed(e); + if (_root is null) + return; + if (Target is not null) + { + _root.ScrollToAnchor(Target); + } + } +} \ No newline at end of file