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