From 881b9ca7d32ea5d19fca4c4941d39f2f44082770 Mon Sep 17 00:00:00 2001 From: Dong Bin Date: Wed, 2 Jul 2025 12:12:09 +0800 Subject: [PATCH 01/18] feat: implementations. --- demo/Ursa.Demo/Pages/AnchorDemo.axaml | 69 +++++++++++++ demo/Ursa.Demo/Pages/AnchorDemo.axaml.cs | 13 +++ .../ViewModels/AnchorDemoViewModel.cs | 8 ++ .../Ursa.Demo/ViewModels/MainViewViewModel.cs | 5 +- demo/Ursa.Demo/ViewModels/MenuViewModel.cs | 10 +- src/Ursa.Themes.Semi/Controls/Anchor.axaml | 15 +++ src/Ursa.Themes.Semi/Controls/_index.axaml | 1 + src/Ursa/Controls/Anchor/Anchor.cs | 99 +++++++++++++++++++ src/Ursa/Controls/Anchor/AnchorItem.cs | 47 +++++++++ 9 files changed, 261 insertions(+), 6 deletions(-) create mode 100644 demo/Ursa.Demo/Pages/AnchorDemo.axaml create mode 100644 demo/Ursa.Demo/Pages/AnchorDemo.axaml.cs create mode 100644 demo/Ursa.Demo/ViewModels/AnchorDemoViewModel.cs create mode 100644 src/Ursa.Themes.Semi/Controls/Anchor.axaml create mode 100644 src/Ursa/Controls/Anchor/Anchor.cs create mode 100644 src/Ursa/Controls/Anchor/AnchorItem.cs 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 From 6deccdc1ac6fb5f417df9af84a4ebe91bd04030a Mon Sep 17 00:00:00 2001 From: Dong Bin Date: Wed, 2 Jul 2025 22:31:16 +0800 Subject: [PATCH 02/18] feat: add mvvm demo. --- demo/Ursa.Demo/Pages/AnchorDemo.axaml | 220 +++++++++++++----- .../ViewModels/AnchorDemoViewModel.cs | 18 +- src/Ursa/Controls/Anchor/Anchor.cs | 50 ++-- src/Ursa/Controls/Anchor/AnchorItem.cs | 14 +- 4 files changed, 220 insertions(+), 82 deletions(-) diff --git a/demo/Ursa.Demo/Pages/AnchorDemo.axaml b/demo/Ursa.Demo/Pages/AnchorDemo.axaml index fce02d2..dc654f2 100644 --- a/demo/Ursa.Demo/Pages/AnchorDemo.axaml +++ b/demo/Ursa.Demo/Pages/AnchorDemo.axaml @@ -5,65 +5,169 @@ xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:u="https://irihi.tech/ursa" + xmlns:viewModels="clr-namespace:Ursa.Demo.ViewModels" d:DesignHeight="450" d:DesignWidth="800" + x:DataType="viewModels:AnchorDemoViewModel" mc:Ignorable="d"> - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/demo/Ursa.Demo/ViewModels/AnchorDemoViewModel.cs b/demo/Ursa.Demo/ViewModels/AnchorDemoViewModel.cs index 37ca6fa..a2c32fc 100644 --- a/demo/Ursa.Demo/ViewModels/AnchorDemoViewModel.cs +++ b/demo/Ursa.Demo/ViewModels/AnchorDemoViewModel.cs @@ -1,8 +1,24 @@ +using System.Collections.Generic; using CommunityToolkit.Mvvm.ComponentModel; namespace Ursa.Demo.ViewModels; public partial class AnchorDemoViewModel: ObservableObject { - + public List AnchorItems { get; } = new() + { + new AnchorItemViewModel { AnchorId = "anchor1", Header = "Anchor 1" }, + new AnchorItemViewModel { AnchorId = "anchor2", Header = "Anchor 2" }, + new AnchorItemViewModel { AnchorId = "anchor3", Header = "Anchor 3" }, + new AnchorItemViewModel { AnchorId = "anchor4", Header = "Anchor 4" }, + new AnchorItemViewModel { AnchorId = "anchor5", Header = "Anchor 5" }, + new AnchorItemViewModel { AnchorId = "anchor6", Header = "Anchor 6" }, + new AnchorItemViewModel { AnchorId = "anchor7", Header = "Anchor 7" }, + }; +} + +public partial class AnchorItemViewModel: ObservableObject +{ + [ObservableProperty] private string? _anchorId; + [ObservableProperty] private string? _header; } \ No newline at end of file diff --git a/src/Ursa/Controls/Anchor/Anchor.cs b/src/Ursa/Controls/Anchor/Anchor.cs index 7c582b8..ef7d401 100644 --- a/src/Ursa/Controls/Anchor/Anchor.cs +++ b/src/Ursa/Controls/Anchor/Anchor.cs @@ -3,7 +3,9 @@ using Avalonia.Animation; using Avalonia.Animation.Easings; using Avalonia.Controls; using Avalonia.Controls.Primitives; +using Avalonia.Interactivity; using Avalonia.Styling; +using Avalonia.VisualTree; namespace Ursa.Controls; @@ -18,11 +20,11 @@ public class Anchor: SelectingItemsControl set => SetValue(TargetContainerProperty, value); } - public static readonly AttachedProperty AnchorIdProperty = - AvaloniaProperty.RegisterAttached("AnchorId"); + 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); + public static void SetAnchorId(Visual obj, string? value) => obj.SetValue(AnchorIdProperty, value); + public static string? GetAnchorId(Visual obj) => obj.GetValue(AnchorIdProperty); protected override bool NeedsContainerOverride(object? item, int index, out object? recycleKey) @@ -40,24 +42,16 @@ public class Anchor: SelectingItemsControl { 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); - } - + var target = TargetContainer.GetVisualDescendants().FirstOrDefault(a=>Anchor.GetAnchorId(a) == anchorId); + if (target is null) return; + ScrollToAnchor(target); } - internal void ScrollToAnchor(Control target) + internal void ScrollToAnchor(Visual target) { if (TargetContainer is null) return; - + TargetContainer.Loaded += OnTargetLoaded; var targetPosition = target.TranslatePoint(new Point(0, 0), TargetContainer); if (targetPosition.HasValue) { @@ -95,5 +89,25 @@ public class Anchor: SelectingItemsControl // TargetContainer.Offset = TargetContainer.Offset.WithY(TargetContainer.Offset.Y + targetPosition.Value.Y); } } - + + private void OnTargetLoaded(object sender, RoutedEventArgs e) + { + if (sender is ScrollViewer scrollViewer) + { + scrollViewer.Loaded -= OnTargetLoaded; + if (scrollViewer.Content is Visual target) + { + var anchorId = GetAnchorId(target); + if (!string.IsNullOrEmpty(anchorId)) + { + ScrollToAnchor(anchorId); + } + } + } + } + + public void InvalidatePositions() + { + + } } \ No newline at end of file diff --git a/src/Ursa/Controls/Anchor/AnchorItem.cs b/src/Ursa/Controls/Anchor/AnchorItem.cs index ae15b15..0f71af2 100644 --- a/src/Ursa/Controls/Anchor/AnchorItem.cs +++ b/src/Ursa/Controls/Anchor/AnchorItem.cs @@ -17,12 +17,12 @@ public class AnchorItem: ContentControl set => SetValue(TargetProperty, value); } - public static readonly StyledProperty AnchorNameProperty = AvaloniaProperty.Register( - nameof(AnchorName)); - public string? AnchorName + public static readonly StyledProperty AnchorIdProperty = AvaloniaProperty.Register( + nameof(AnchorId)); + public string? AnchorId { - get => GetValue(AnchorNameProperty); - set => SetValue(AnchorNameProperty, value); + get => GetValue(AnchorIdProperty); + set => SetValue(AnchorIdProperty, value); } private Anchor? _root; @@ -43,5 +43,9 @@ public class AnchorItem: ContentControl { _root.ScrollToAnchor(Target); } + else if (!string.IsNullOrEmpty(AnchorId)) + { + _root.ScrollToAnchor(AnchorId); + } } } \ No newline at end of file From 817eb9acc9cb1b0c16fc07f9d05425ebf8f95371 Mon Sep 17 00:00:00 2001 From: Dong Bin Date: Thu, 3 Jul 2025 23:06:22 +0800 Subject: [PATCH 03/18] feat: WIP. --- Ursa.sln | 3 +- demo/Ursa.Demo/Pages/AnchorDemo.axaml | 37 ++++++----- src/Ursa.Themes.Semi/Controls/Anchor.axaml | 12 ++++ src/Ursa/Controls/Anchor/Anchor.cs | 66 +++++++++++++------ src/Ursa/Controls/Anchor/AnchorItem.cs | 76 ++++++++++++++++------ 5 files changed, 135 insertions(+), 59 deletions(-) diff --git a/Ursa.sln b/Ursa.sln index 695f774..524363d 100644 --- a/Ursa.sln +++ b/Ursa.sln @@ -42,8 +42,8 @@ EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "GitHub Action", "GitHub Action", "{66123AC1-7C8C-4AA0-BBDB-5CC3E647A741}" ProjectSection(SolutionItems) = preProject .github\workflows\deploy.yml = .github\workflows\deploy.yml - .github\workflows\pack.yml = .github\workflows\pack.yml .github\workflows\pack-nightly.yml = .github\workflows\pack-nightly.yml + .github\workflows\pack.yml = .github\workflows\pack.yml .github\workflows\publish.yml = .github\workflows\publish.yml .github\workflows\release-tag.yml = .github\workflows\release-tag.yml .github\workflows\test.yml = .github\workflows\test.yml @@ -69,6 +69,7 @@ Global {53B5F277-3AEB-4661-ACAE-15CFFF2ED800}.Release|Any CPU.Build.0 = Release|Any CPU {3FC76CD9-CE5D-4804-A8D6-4E292EB296AA}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {3FC76CD9-CE5D-4804-A8D6-4E292EB296AA}.Debug|Any CPU.Build.0 = Debug|Any CPU + {3FC76CD9-CE5D-4804-A8D6-4E292EB296AA}.Debug|Any CPU.Deploy.0 = Debug|Any CPU {3FC76CD9-CE5D-4804-A8D6-4E292EB296AA}.Release|Any CPU.ActiveCfg = Release|Any CPU {3FC76CD9-CE5D-4804-A8D6-4E292EB296AA}.Release|Any CPU.Build.0 = Release|Any CPU {B6BAB821-A9FE-44F3-B9CD-06E27FDB63F6}.Debug|Any CPU.ActiveCfg = Debug|Any CPU diff --git a/demo/Ursa.Demo/Pages/AnchorDemo.axaml b/demo/Ursa.Demo/Pages/AnchorDemo.axaml index dc654f2..e548f18 100644 --- a/demo/Ursa.Demo/Pages/AnchorDemo.axaml +++ b/demo/Ursa.Demo/Pages/AnchorDemo.axaml @@ -25,49 +25,48 @@ - + @@ -80,13 +79,16 @@ Width="200" Margin="24" TargetContainer="{Binding ElementName=container1}"> - - - - - - - + + + + + + + + + + @@ -157,13 +159,14 @@ diff --git a/src/Ursa.Themes.Semi/Controls/Anchor.axaml b/src/Ursa.Themes.Semi/Controls/Anchor.axaml index 5422215..75dc5ba 100644 --- a/src/Ursa.Themes.Semi/Controls/Anchor.axaml +++ b/src/Ursa.Themes.Semi/Controls/Anchor.axaml @@ -12,4 +12,16 @@ + + + + + + + + + + + + diff --git a/src/Ursa/Controls/Anchor/Anchor.cs b/src/Ursa/Controls/Anchor/Anchor.cs index ef7d401..966a894 100644 --- a/src/Ursa/Controls/Anchor/Anchor.cs +++ b/src/Ursa/Controls/Anchor/Anchor.cs @@ -46,12 +46,13 @@ public class Anchor: SelectingItemsControl if (target is null) return; ScrollToAnchor(target); } + + private CancellationTokenSource _cts = new(); - internal void ScrollToAnchor(Visual target) + private void ScrollToAnchor(Visual target) { if (TargetContainer is null) return; - TargetContainer.Loaded += OnTargetLoaded; var targetPosition = target.TranslatePoint(new Point(0, 0), TargetContainer); if (targetPosition.HasValue) { @@ -61,6 +62,7 @@ public class Anchor: SelectingItemsControl { to = TargetContainer.Extent.Height - TargetContainer.Bounds.Height; } + if (from == to) return; Animation animation = new Animation() { Duration = TimeSpan.FromSeconds(0.3), @@ -85,29 +87,51 @@ public class Anchor: SelectingItemsControl } }; - animation.RunAsync(TargetContainer); - // TargetContainer.Offset = TargetContainer.Offset.WithY(TargetContainer.Offset.Y + targetPosition.Value.Y); + _cts.Cancel(); + _cts = new CancellationTokenSource(); + animation.RunAsync(TargetContainer, _cts.Token); } } - - private void OnTargetLoaded(object sender, RoutedEventArgs e) - { - if (sender is ScrollViewer scrollViewer) - { - scrollViewer.Loaded -= OnTargetLoaded; - if (scrollViewer.Content is Visual target) - { - var anchorId = GetAnchorId(target); - if (!string.IsNullOrEmpty(anchorId)) - { - ScrollToAnchor(anchorId); - } - } - } - } - + public void InvalidatePositions() { } + + protected override void OnLoaded(RoutedEventArgs e) + { + base.OnLoaded(e); + var items = this.GetVisualDescendants().OfType().ToList(); + var target = this.TargetContainer; + if (target is null) return; + var targetItems = target.GetVisualDescendants().Where(a => Anchor.GetAnchorId(a) is not null).ToList(); + var tops = targetItems.Select(a => (a.TransformToVisual(target)?.M32, GetAnchorId(a))); + var isloaded = TargetContainer?.IsLoaded; + TargetContainer?.AddHandler(ScrollViewer.ScrollChangedEvent, OnScrollChanged); + } + + private void OnScrollChanged(object? sender, ScrollChangedEventArgs e) + { + + } + + internal Control CreateContainerForItemOverride_INTERNAL(object? item, int index, object? recycleKey) + { + return CreateContainerForItemOverride(item, index, recycleKey); + } + + internal bool NeedsContainerOverride_INTERNAL(object? item, int index, out object? recycleKey) + { + return NeedsContainerOverride(item, index, out recycleKey); + } + + internal void PrepareContainerForItemOverride_INTERNAL(Control container, object? item, int index) + { + PrepareContainerForItemOverride(container, item, index); + } + + internal void ContainerForItemPreparedOverride_INTERNAL(Control container, object? item, int index) + { + ContainerForItemPreparedOverride(container, item, index); + } } \ No newline at end of file diff --git a/src/Ursa/Controls/Anchor/AnchorItem.cs b/src/Ursa/Controls/Anchor/AnchorItem.cs index 0f71af2..9799c31 100644 --- a/src/Ursa/Controls/Anchor/AnchorItem.cs +++ b/src/Ursa/Controls/Anchor/AnchorItem.cs @@ -1,51 +1,87 @@ -using System.Windows.Input; using Avalonia; using Avalonia.Controls; +using Avalonia.Controls.Mixins; +using Avalonia.Controls.Primitives; +using Avalonia.Controls.Templates; using Avalonia.Input; -using Avalonia.VisualTree; +using Avalonia.LogicalTree; namespace Ursa.Controls; -public class AnchorItem: ContentControl +public class AnchorItem : HeaderedItemsControl, ISelectable { - public static readonly StyledProperty TargetProperty = AvaloniaProperty.Register( - nameof(Target)); - - public Control? Target - { - get => GetValue(TargetProperty); - set => SetValue(TargetProperty, value); - } - public static readonly StyledProperty AnchorIdProperty = AvaloniaProperty.Register( nameof(AnchorId)); + + public static readonly StyledProperty IsSelectedProperty = + SelectingItemsControl.IsSelectedProperty.AddOwner(); + + private static readonly FuncTemplate DefaultPanel = + new(() => new StackPanel()); + + private Anchor? _root; + + static AnchorItem() + { + SelectableMixin.Attach(IsSelectedProperty); + PressedMixin.Attach(); + ItemsPanelProperty.OverrideDefaultValue(DefaultPanel); + } + public string? AnchorId { get => GetValue(AnchorIdProperty); set => SetValue(AnchorIdProperty, value); } - private Anchor? _root; + public bool IsSelected + { + get => GetValue(IsSelectedProperty); + set => SetValue(IsSelectedProperty, value); + } protected override void OnAttachedToVisualTree(VisualTreeAttachmentEventArgs e) { base.OnAttachedToVisualTree(e); - _root = this.FindAncestorOfType() ?? - throw new InvalidOperationException("AnchorItem must be inside an Anchor control."); + _root = this.GetLogicalAncestors().OfType().FirstOrDefault(); } protected override void OnPointerPressed(PointerPressedEventArgs e) { + var item = new TreeViewItem(); base.OnPointerPressed(e); + if (e.Handled) return; if (_root is null) return; - if (Target is not null) - { - _root.ScrollToAnchor(Target); - } - else if (!string.IsNullOrEmpty(AnchorId)) + if (!string.IsNullOrEmpty(AnchorId)) { _root.ScrollToAnchor(AnchorId); + e.Handled = true; } } + + protected override Control CreateContainerForItemOverride(object? item, int index, object? recycleKey) + { + return EnsureRoot().CreateContainerForItemOverride_INTERNAL(item, index, recycleKey); + } + + protected override bool NeedsContainerOverride(object? item, int index, out object? recycleKey) + { + return EnsureRoot().NeedsContainerOverride_INTERNAL(item, index, out recycleKey); + } + + protected override void PrepareContainerForItemOverride(Control container, object? item, int index) + { + EnsureRoot().PrepareContainerForItemOverride_INTERNAL(container, item, index); + } + + protected override void ContainerForItemPreparedOverride(Control container, object? item, int index) + { + EnsureRoot().ContainerForItemPreparedOverride_INTERNAL(container, item, index); + } + + private Anchor EnsureRoot() + { + return _root ?? throw new InvalidOperationException("AnchorItem must be inside an Anchor control."); + } } \ No newline at end of file From dbc41249d8dd9a55c078750edbd0f15d80f1b452 Mon Sep 17 00:00:00 2001 From: Dong Bin Date: Fri, 4 Jul 2025 16:09:33 +0800 Subject: [PATCH 04/18] feat: implement tree like visual. --- demo/Ursa.Demo/Pages/AnchorDemo.axaml | 27 ++++++++++++++++++- .../ViewModels/AnchorDemoViewModel.cs | 13 ++++++++- src/Ursa.Themes.Semi/Controls/Anchor.axaml | 2 +- src/Ursa/Controls/Anchor/AnchorItem.cs | 11 +++++++- 4 files changed, 49 insertions(+), 4 deletions(-) diff --git a/demo/Ursa.Demo/Pages/AnchorDemo.axaml b/demo/Ursa.Demo/Pages/AnchorDemo.axaml index e548f18..cfcbc10 100644 --- a/demo/Ursa.Demo/Pages/AnchorDemo.axaml +++ b/demo/Ursa.Demo/Pages/AnchorDemo.axaml @@ -127,6 +127,27 @@ Background="{DynamicResource SemiPurple1}"> + + + + + + + + + + + + + + diff --git a/demo/Ursa.Demo/ViewModels/AnchorDemoViewModel.cs b/demo/Ursa.Demo/ViewModels/AnchorDemoViewModel.cs index a2c32fc..ed74715 100644 --- a/demo/Ursa.Demo/ViewModels/AnchorDemoViewModel.cs +++ b/demo/Ursa.Demo/ViewModels/AnchorDemoViewModel.cs @@ -1,4 +1,5 @@ using System.Collections.Generic; +using System.Collections.ObjectModel; using CommunityToolkit.Mvvm.ComponentModel; namespace Ursa.Demo.ViewModels; @@ -9,7 +10,16 @@ public partial class AnchorDemoViewModel: ObservableObject { new AnchorItemViewModel { AnchorId = "anchor1", Header = "Anchor 1" }, new AnchorItemViewModel { AnchorId = "anchor2", Header = "Anchor 2" }, - new AnchorItemViewModel { AnchorId = "anchor3", Header = "Anchor 3" }, + new AnchorItemViewModel + { + AnchorId = "anchor3", Header = "Anchor 3", + Children = + [ + new AnchorItemViewModel() { AnchorId = "anchor3-1", Header = "Anchor 3.1" }, + new AnchorItemViewModel() { AnchorId = "anchor3-2", Header = "Anchor 3.2" }, + new AnchorItemViewModel() { AnchorId = "anchor3-3", Header = "Anchor 3.3" } + ] + }, new AnchorItemViewModel { AnchorId = "anchor4", Header = "Anchor 4" }, new AnchorItemViewModel { AnchorId = "anchor5", Header = "Anchor 5" }, new AnchorItemViewModel { AnchorId = "anchor6", Header = "Anchor 6" }, @@ -21,4 +31,5 @@ public partial class AnchorItemViewModel: ObservableObject { [ObservableProperty] private string? _anchorId; [ObservableProperty] private string? _header; + public ObservableCollection? Children { get; set; } } \ No newline at end of file diff --git a/src/Ursa.Themes.Semi/Controls/Anchor.axaml b/src/Ursa.Themes.Semi/Controls/Anchor.axaml index 75dc5ba..510c98e 100644 --- a/src/Ursa.Themes.Semi/Controls/Anchor.axaml +++ b/src/Ursa.Themes.Semi/Controls/Anchor.axaml @@ -18,7 +18,7 @@ - + diff --git a/src/Ursa/Controls/Anchor/AnchorItem.cs b/src/Ursa/Controls/Anchor/AnchorItem.cs index 9799c31..446797c 100644 --- a/src/Ursa/Controls/Anchor/AnchorItem.cs +++ b/src/Ursa/Controls/Anchor/AnchorItem.cs @@ -44,11 +44,20 @@ public class AnchorItem : HeaderedItemsControl, ISelectable { base.OnAttachedToVisualTree(e); _root = this.GetLogicalAncestors().OfType().FirstOrDefault(); + if (ItemTemplate is null && _root?.ItemTemplate is not null) + { + SetCurrentValue(ItemTemplateProperty, _root.ItemTemplate); + } + + if (ItemContainerTheme is null && _root?.ItemContainerTheme is not null) + { + SetCurrentValue(ItemContainerThemeProperty, _root.ItemContainerTheme); + } } protected override void OnPointerPressed(PointerPressedEventArgs e) { - var item = new TreeViewItem(); + // var item = new TreeViewItem(); base.OnPointerPressed(e); if (e.Handled) return; if (_root is null) From 85605b5faf5d4dbef53dc49050ddbddb0da6e691 Mon Sep 17 00:00:00 2001 From: Dong Bin Date: Sat, 5 Jul 2025 01:55:17 +0800 Subject: [PATCH 05/18] feat: implement scroll detection. --- src/Ursa.Themes.Semi/Controls/Anchor.axaml | 8 +++- src/Ursa/Common/VisualHelpers.cs | 13 ++++++ src/Ursa/Controls/Anchor/Anchor.cs | 47 ++++++++++++++++++++-- src/Ursa/Controls/Anchor/AnchorItem.cs | 1 + 4 files changed, 63 insertions(+), 6 deletions(-) create mode 100644 src/Ursa/Common/VisualHelpers.cs diff --git a/src/Ursa.Themes.Semi/Controls/Anchor.axaml b/src/Ursa.Themes.Semi/Controls/Anchor.axaml index 510c98e..1145346 100644 --- a/src/Ursa.Themes.Semi/Controls/Anchor.axaml +++ b/src/Ursa.Themes.Semi/Controls/Anchor.axaml @@ -1,7 +1,8 @@ + xmlns:u="https://irihi.tech/ursa" + xmlns:iri="https://irihi.tech/shared"> @@ -18,10 +19,13 @@ - + + diff --git a/src/Ursa/Common/VisualHelpers.cs b/src/Ursa/Common/VisualHelpers.cs new file mode 100644 index 0000000..37195ac --- /dev/null +++ b/src/Ursa/Common/VisualHelpers.cs @@ -0,0 +1,13 @@ +using Avalonia; +using Avalonia.VisualTree; + +namespace Ursa.Common; + +public static class VisualHelpers +{ + public static T? GetContainerFromEventSource(this Visual? source) + { + var item = source.GetSelfAndVisualAncestors().OfType().FirstOrDefault(); + return item; + } +} \ No newline at end of file diff --git a/src/Ursa/Controls/Anchor/Anchor.cs b/src/Ursa/Controls/Anchor/Anchor.cs index 966a894..58f1d82 100644 --- a/src/Ursa/Controls/Anchor/Anchor.cs +++ b/src/Ursa/Controls/Anchor/Anchor.cs @@ -1,15 +1,23 @@ +using System.Diagnostics; using Avalonia; using Avalonia.Animation; using Avalonia.Animation.Easings; using Avalonia.Controls; -using Avalonia.Controls.Primitives; +using Avalonia.Input; using Avalonia.Interactivity; using Avalonia.Styling; using Avalonia.VisualTree; +using Ursa.Common; namespace Ursa.Controls; -public class Anchor: SelectingItemsControl +/// +/// Some basic assumptions: This should not be a regular SelectingItemsControl, because it does not support multiple selections. +/// Selection should not be exposed to the user, it is only used to determine which item is currently selected. +/// The manipulation of container selection should be simplified. +/// Scroll event of TargetContainer also triggers selection change. +/// +public class Anchor: ItemsControl { public static readonly StyledProperty TargetContainerProperty = AvaloniaProperty.Register( nameof(TargetContainer)); @@ -40,6 +48,7 @@ public class Anchor: SelectingItemsControl internal void ScrollToAnchor(string anchorId) { + return; if (TargetContainer is null) return; var target = TargetContainer.GetVisualDescendants().FirstOrDefault(a=>Anchor.GetAnchorId(a) == anchorId); @@ -48,11 +57,13 @@ public class Anchor: SelectingItemsControl } private CancellationTokenSource _cts = new(); + private bool _scrollingFromSelection = false; private void ScrollToAnchor(Visual target) { if (TargetContainer is null) return; + var targetPosition = target.TranslatePoint(new Point(0, 0), TargetContainer); if (targetPosition.HasValue) { @@ -89,7 +100,10 @@ public class Anchor: SelectingItemsControl }; _cts.Cancel(); _cts = new CancellationTokenSource(); - animation.RunAsync(TargetContainer, _cts.Token); + var token = _cts.Token; + token.Register(_ => _scrollingFromSelection = false, null); + _scrollingFromSelection = true; + animation.RunAsync(TargetContainer, token).ContinueWith(_ => _scrollingFromSelection = false, token); } } @@ -112,9 +126,30 @@ public class Anchor: SelectingItemsControl private void OnScrollChanged(object? sender, ScrollChangedEventArgs e) { - + if (_scrollingFromSelection) return; + Debug.WriteLine("Scroll changed"); } + protected override void OnPointerPressed(PointerPressedEventArgs e) + { + base.OnPointerPressed(e); + var source = (e.Source as Visual).GetContainerFromEventSource(); + if (source is null) return; + if (_selectedContainer is not null) + { + _selectedContainer.IsSelected = false; + } + source.IsSelected = true; + _selectedContainer = source; + var target = TargetContainer?.GetVisualDescendants() + .FirstOrDefault(a => Anchor.GetAnchorId(a) == source?.AnchorId); + if (target is null) return; + ScrollToAnchor(target); + } + + /// + /// This method is used to expose the protected CreateContainerForItemOverride method to the AnchorItem class. + /// internal Control CreateContainerForItemOverride_INTERNAL(object? item, int index, object? recycleKey) { return CreateContainerForItemOverride(item, index, recycleKey); @@ -134,4 +169,8 @@ public class Anchor: SelectingItemsControl { ContainerForItemPreparedOverride(container, item, index); } + + internal AnchorItem? _selectedContainer; + + } \ No newline at end of file diff --git a/src/Ursa/Controls/Anchor/AnchorItem.cs b/src/Ursa/Controls/Anchor/AnchorItem.cs index 446797c..dfbd522 100644 --- a/src/Ursa/Controls/Anchor/AnchorItem.cs +++ b/src/Ursa/Controls/Anchor/AnchorItem.cs @@ -59,6 +59,7 @@ public class AnchorItem : HeaderedItemsControl, ISelectable { // var item = new TreeViewItem(); base.OnPointerPressed(e); + return; if (e.Handled) return; if (_root is null) return; From 68f95826c5323e78539807db03039cd10af12fdb Mon Sep 17 00:00:00 2001 From: Dong Bin Date: Sun, 6 Jul 2025 01:17:08 +0800 Subject: [PATCH 06/18] feat: scroll to select. --- src/Ursa/Controls/Anchor/Anchor.cs | 74 +++++++++++++++++++------- src/Ursa/Controls/Anchor/AnchorItem.cs | 15 ------ 2 files changed, 56 insertions(+), 33 deletions(-) diff --git a/src/Ursa/Controls/Anchor/Anchor.cs b/src/Ursa/Controls/Anchor/Anchor.cs index 58f1d82..5f3d5c8 100644 --- a/src/Ursa/Controls/Anchor/Anchor.cs +++ b/src/Ursa/Controls/Anchor/Anchor.cs @@ -45,16 +45,6 @@ public class Anchor: ItemsControl var i = new AnchorItem(); return i; } - - internal void ScrollToAnchor(string anchorId) - { - return; - if (TargetContainer is null) - return; - var target = TargetContainer.GetVisualDescendants().FirstOrDefault(a=>Anchor.GetAnchorId(a) == anchorId); - if (target is null) return; - ScrollToAnchor(target); - } private CancellationTokenSource _cts = new(); private bool _scrollingFromSelection = false; @@ -109,7 +99,29 @@ public class Anchor: ItemsControl public void InvalidatePositions() { - + InvalidateAnchorPositions(); + MarkSelectedContainerByPosition(); + } + + private List<(string, double)> _positions = []; + + internal void InvalidateAnchorPositions() + { + if (TargetContainer is null) return; + var items = TargetContainer.GetVisualDescendants().Where(a => GetAnchorId(a) is not null); + List<(string, double)> positions = new List<(string, double)>(); + foreach (var item in items) + { + var anchorId = GetAnchorId(item); + if (anchorId is null) continue; + var position = item.TransformToVisual(TargetContainer)?.M32; + if (position.HasValue) + { + positions.Add((anchorId, position.Value)); + } + } + positions.Sort((a, b) => a.Item2.CompareTo(b.Item2)); + _positions = positions; } protected override void OnLoaded(RoutedEventArgs e) @@ -122,12 +134,22 @@ public class Anchor: ItemsControl var tops = targetItems.Select(a => (a.TransformToVisual(target)?.M32, GetAnchorId(a))); var isloaded = TargetContainer?.IsLoaded; TargetContainer?.AddHandler(ScrollViewer.ScrollChangedEvent, OnScrollChanged); + if (isloaded is true) + { + InvalidateAnchorPositions(); + } + else + { + TargetContainer.Loaded += (s, args) => InvalidateAnchorPositions(); + } + + MarkSelectedContainerByPosition(); } private void OnScrollChanged(object? sender, ScrollChangedEventArgs e) { if (_scrollingFromSelection) return; - Debug.WriteLine("Scroll changed"); + MarkSelectedContainerByPosition(); } protected override void OnPointerPressed(PointerPressedEventArgs e) @@ -135,12 +157,7 @@ public class Anchor: ItemsControl base.OnPointerPressed(e); var source = (e.Source as Visual).GetContainerFromEventSource(); if (source is null) return; - if (_selectedContainer is not null) - { - _selectedContainer.IsSelected = false; - } - source.IsSelected = true; - _selectedContainer = source; + MarkSelectedContainer(source); var target = TargetContainer?.GetVisualDescendants() .FirstOrDefault(a => Anchor.GetAnchorId(a) == source?.AnchorId); if (target is null) return; @@ -172,5 +189,26 @@ public class Anchor: ItemsControl internal AnchorItem? _selectedContainer; + internal void MarkSelectedContainer(AnchorItem? item) + { + var oldValue = _selectedContainer; + var newValue = item; + if (oldValue == newValue) return; + _selectedContainer?.SetValue(AnchorItem.IsSelectedProperty, false); + _selectedContainer = newValue; + _selectedContainer?.SetValue(AnchorItem.IsSelectedProperty, true); + } + + internal void MarkSelectedContainerByPosition() + { + if (TargetContainer is null) return; + var top = TargetContainer.Offset.Y; + var topAnchorId = _positions.LastOrDefault(a => a.Item2 <= top).Item1; + if (topAnchorId is null) return; + var item = this.GetVisualDescendants().OfType() + .FirstOrDefault(a => a.AnchorId == topAnchorId); + if (item is null) return; + MarkSelectedContainer(item); + } } \ No newline at end of file diff --git a/src/Ursa/Controls/Anchor/AnchorItem.cs b/src/Ursa/Controls/Anchor/AnchorItem.cs index dfbd522..381e284 100644 --- a/src/Ursa/Controls/Anchor/AnchorItem.cs +++ b/src/Ursa/Controls/Anchor/AnchorItem.cs @@ -54,21 +54,6 @@ public class AnchorItem : HeaderedItemsControl, ISelectable SetCurrentValue(ItemContainerThemeProperty, _root.ItemContainerTheme); } } - - protected override void OnPointerPressed(PointerPressedEventArgs e) - { - // var item = new TreeViewItem(); - base.OnPointerPressed(e); - return; - if (e.Handled) return; - if (_root is null) - return; - if (!string.IsNullOrEmpty(AnchorId)) - { - _root.ScrollToAnchor(AnchorId); - e.Handled = true; - } - } protected override Control CreateContainerForItemOverride(object? item, int index, object? recycleKey) { From fb799636d99b1b493d22d9796d0e6d44157870a1 Mon Sep 17 00:00:00 2001 From: Dong Bin Date: Tue, 8 Jul 2025 13:36:11 +0800 Subject: [PATCH 07/18] feat: update selection handling, fix initial offset calculation. --- src/Ursa.Themes.Semi/Controls/Anchor.axaml | 53 ++++++++++++++----- ...avigationMenuItemLevelToMarginConverter.cs | 23 -------- .../Converters/TreeLevelToMarginConverter.cs | 17 ++++++ .../Themes/Shared/Anchor.axaml | 4 ++ .../Themes/Shared/_index.axaml | 1 + src/Ursa/Common/LogicalHelpers.cs | 21 ++++++++ src/Ursa/Controls/Anchor/Anchor.cs | 33 ++++++------ src/Ursa/Controls/Anchor/AnchorItem.cs | 21 +++++--- 8 files changed, 116 insertions(+), 57 deletions(-) delete mode 100644 src/Ursa.Themes.Semi/Converters/NavigationMenuItemLevelToMarginConverter.cs create mode 100644 src/Ursa.Themes.Semi/Converters/TreeLevelToMarginConverter.cs create mode 100644 src/Ursa.Themes.Semi/Themes/Shared/Anchor.axaml create mode 100644 src/Ursa/Common/LogicalHelpers.cs diff --git a/src/Ursa.Themes.Semi/Controls/Anchor.axaml b/src/Ursa.Themes.Semi/Controls/Anchor.axaml index 1145346..098f292 100644 --- a/src/Ursa.Themes.Semi/Controls/Anchor.axaml +++ b/src/Ursa.Themes.Semi/Controls/Anchor.axaml @@ -1,31 +1,60 @@ + xmlns:converters="clr-namespace:Ursa.Themes.Semi.Converters" + xmlns:iri="https://irihi.tech/shared" + xmlns:u="https://irihi.tech/ursa"> + - + + + + - - + + + - - + + + + + + + + + + + + + + - diff --git a/src/Ursa.Themes.Semi/Converters/NavigationMenuItemLevelToMarginConverter.cs b/src/Ursa.Themes.Semi/Converters/NavigationMenuItemLevelToMarginConverter.cs deleted file mode 100644 index bcacb57..0000000 --- a/src/Ursa.Themes.Semi/Converters/NavigationMenuItemLevelToMarginConverter.cs +++ /dev/null @@ -1,23 +0,0 @@ -using System.Globalization; -using Avalonia; -using Avalonia.Data.Converters; - -namespace Ursa.Themes.Semi.Converters; - -public class NavigationMenuItemLevelToMarginConverter: IMultiValueConverter -{ - public int Indent { get; set; } - - public object? Convert(IList values, Type targetType, object? parameter, CultureInfo culture) - { - if (values[0] is int i && values[1] is bool b) - { - if (b) - { - return new Thickness(); - } - return new Thickness((i-1) * Indent, 0, 0, 0); - } - return new Thickness(); - } -} \ No newline at end of file diff --git a/src/Ursa.Themes.Semi/Converters/TreeLevelToMarginConverter.cs b/src/Ursa.Themes.Semi/Converters/TreeLevelToMarginConverter.cs new file mode 100644 index 0000000..15ca142 --- /dev/null +++ b/src/Ursa.Themes.Semi/Converters/TreeLevelToMarginConverter.cs @@ -0,0 +1,17 @@ +using System.Globalization; +using Avalonia; +using Avalonia.Data.Converters; + +namespace Ursa.Themes.Semi.Converters; + +public class TreeLevelToMarginConverter: IMultiValueConverter +{ + public object? Convert(IList values, Type targetType, object? parameter, CultureInfo culture) + { + if (values[0] is int i && values[1] is double indent) + { + return new Thickness((i-1) * indent, 0, 0, 0); + } + return new Thickness(); + } +} \ No newline at end of file diff --git a/src/Ursa.Themes.Semi/Themes/Shared/Anchor.axaml b/src/Ursa.Themes.Semi/Themes/Shared/Anchor.axaml new file mode 100644 index 0000000..3f22c22 --- /dev/null +++ b/src/Ursa.Themes.Semi/Themes/Shared/Anchor.axaml @@ -0,0 +1,4 @@ + + 12 + diff --git a/src/Ursa.Themes.Semi/Themes/Shared/_index.axaml b/src/Ursa.Themes.Semi/Themes/Shared/_index.axaml index 01c7e1a..7a665f6 100644 --- a/src/Ursa.Themes.Semi/Themes/Shared/_index.axaml +++ b/src/Ursa.Themes.Semi/Themes/Shared/_index.axaml @@ -1,6 +1,7 @@ + diff --git a/src/Ursa/Common/LogicalHelpers.cs b/src/Ursa/Common/LogicalHelpers.cs new file mode 100644 index 0000000..4a17487 --- /dev/null +++ b/src/Ursa/Common/LogicalHelpers.cs @@ -0,0 +1,21 @@ +using Avalonia.LogicalTree; +using Ursa.Controls; + +namespace Ursa.Common; + +public static class LogicalHelpers +{ + public static int CalculateDistanceFromLogicalParent(TItem? item, int @default = -1) + where T : class + where TItem : ILogical + { + var result = 0; + ILogical? logical = item; + while (logical is not null and not T) + { + if (logical is TItem) result++; + logical = logical.LogicalParent; + } + return item is not null ? result : @default; + } +} \ No newline at end of file diff --git a/src/Ursa/Controls/Anchor/Anchor.cs b/src/Ursa/Controls/Anchor/Anchor.cs index 5f3d5c8..7a75b93 100644 --- a/src/Ursa/Controls/Anchor/Anchor.cs +++ b/src/Ursa/Controls/Anchor/Anchor.cs @@ -1,4 +1,3 @@ -using System.Diagnostics; using Avalonia; using Avalonia.Animation; using Avalonia.Animation.Easings; @@ -47,7 +46,7 @@ public class Anchor: ItemsControl } private CancellationTokenSource _cts = new(); - private bool _scrollingFromSelection = false; + private bool _scrollingFromSelection; private void ScrollToAnchor(Visual target) { @@ -114,7 +113,7 @@ public class Anchor: ItemsControl { var anchorId = GetAnchorId(item); if (anchorId is null) continue; - var position = item.TransformToVisual(TargetContainer)?.M32; + var position = item.TransformToVisual(TargetContainer)?.M32 + TargetContainer.Offset.Y; if (position.HasValue) { positions.Add((anchorId, position.Value)); @@ -127,22 +126,14 @@ public class Anchor: ItemsControl protected override void OnLoaded(RoutedEventArgs e) { base.OnLoaded(e); - var items = this.GetVisualDescendants().OfType().ToList(); var target = this.TargetContainer; if (target is null) return; - var targetItems = target.GetVisualDescendants().Where(a => Anchor.GetAnchorId(a) is not null).ToList(); - var tops = targetItems.Select(a => (a.TransformToVisual(target)?.M32, GetAnchorId(a))); - var isloaded = TargetContainer?.IsLoaded; TargetContainer?.AddHandler(ScrollViewer.ScrollChangedEvent, OnScrollChanged); - if (isloaded is true) + TargetContainer?.AddHandler(LoadedEvent, OnTargetContainerLoaded); + if (TargetContainer?.IsLoaded == true) { InvalidateAnchorPositions(); } - else - { - TargetContainer.Loaded += (s, args) => InvalidateAnchorPositions(); - } - MarkSelectedContainerByPosition(); } @@ -159,7 +150,7 @@ public class Anchor: ItemsControl if (source is null) return; MarkSelectedContainer(source); var target = TargetContainer?.GetVisualDescendants() - .FirstOrDefault(a => Anchor.GetAnchorId(a) == source?.AnchorId); + .FirstOrDefault(a => Anchor.GetAnchorId(a) == source.AnchorId); if (target is null) return; ScrollToAnchor(target); } @@ -187,7 +178,7 @@ public class Anchor: ItemsControl ContainerForItemPreparedOverride(container, item, index); } - internal AnchorItem? _selectedContainer; + private AnchorItem? _selectedContainer; internal void MarkSelectedContainer(AnchorItem? item) { @@ -210,5 +201,15 @@ public class Anchor: ItemsControl if (item is null) return; MarkSelectedContainer(item); } - + + protected override void OnUnloaded(RoutedEventArgs e) + { + base.OnUnloaded(e); + TargetContainer?.RemoveHandler(UnloadedEvent, OnTargetContainerLoaded); + } + + private void OnTargetContainerLoaded(object? sender, RoutedEventArgs e) + { + InvalidateAnchorPositions(); + } } \ No newline at end of file diff --git a/src/Ursa/Controls/Anchor/AnchorItem.cs b/src/Ursa/Controls/Anchor/AnchorItem.cs index 381e284..6986d6f 100644 --- a/src/Ursa/Controls/Anchor/AnchorItem.cs +++ b/src/Ursa/Controls/Anchor/AnchorItem.cs @@ -3,8 +3,8 @@ using Avalonia.Controls; using Avalonia.Controls.Mixins; using Avalonia.Controls.Primitives; using Avalonia.Controls.Templates; -using Avalonia.Input; using Avalonia.LogicalTree; +using Ursa.Common; namespace Ursa.Controls; @@ -19,6 +19,12 @@ public class AnchorItem : HeaderedItemsControl, ISelectable private static readonly FuncTemplate DefaultPanel = new(() => new StackPanel()); + internal static readonly DirectProperty LevelProperty = + AvaloniaProperty.RegisterDirect( + nameof(Level), o => o.Level, (o, v) => o.Level = v); + + private int _level; + private Anchor? _root; static AnchorItem() @@ -28,6 +34,12 @@ public class AnchorItem : HeaderedItemsControl, ISelectable ItemsPanelProperty.OverrideDefaultValue(DefaultPanel); } + public int Level + { + get => _level; + set => SetAndRaise(LevelProperty, ref _level, value); + } + public string? AnchorId { get => GetValue(AnchorIdProperty); @@ -44,17 +56,14 @@ public class AnchorItem : HeaderedItemsControl, ISelectable { base.OnAttachedToVisualTree(e); _root = this.GetLogicalAncestors().OfType().FirstOrDefault(); + Level = LogicalHelpers.CalculateDistanceFromLogicalParent(this); if (ItemTemplate is null && _root?.ItemTemplate is not null) - { SetCurrentValue(ItemTemplateProperty, _root.ItemTemplate); - } if (ItemContainerTheme is null && _root?.ItemContainerTheme is not null) - { SetCurrentValue(ItemContainerThemeProperty, _root.ItemContainerTheme); - } } - + protected override Control CreateContainerForItemOverride(object? item, int index, object? recycleKey) { return EnsureRoot().CreateContainerForItemOverride_INTERNAL(item, index, recycleKey); From 85016c9e37d6604304d051b6e3e5f1b905233c8f Mon Sep 17 00:00:00 2001 From: Dong Bin Date: Tue, 8 Jul 2025 14:58:58 +0800 Subject: [PATCH 08/18] feat: add inheritance. --- src/Ursa.Themes.Semi/Controls/Anchor.axaml | 36 ++++++- .../Themes/Shared/Anchor.axaml | 2 + src/Ursa/Controls/Anchor/Anchor.cs | 100 +++++++++--------- src/Ursa/Controls/Anchor/AnchorItem.cs | 2 +- 4 files changed, 82 insertions(+), 58 deletions(-) diff --git a/src/Ursa.Themes.Semi/Controls/Anchor.axaml b/src/Ursa.Themes.Semi/Controls/Anchor.axaml index 098f292..108f37b 100644 --- a/src/Ursa.Themes.Semi/Controls/Anchor.axaml +++ b/src/Ursa.Themes.Semi/Controls/Anchor.axaml @@ -6,11 +6,14 @@ xmlns:u="https://irihi.tech/ursa"> + + @@ -21,23 +24,30 @@ + - + + - - + @@ -53,8 +63,24 @@ - + + + + + diff --git a/src/Ursa.Themes.Semi/Themes/Shared/Anchor.axaml b/src/Ursa.Themes.Semi/Themes/Shared/Anchor.axaml index 3f22c22..cffb17e 100644 --- a/src/Ursa.Themes.Semi/Themes/Shared/Anchor.axaml +++ b/src/Ursa.Themes.Semi/Themes/Shared/Anchor.axaml @@ -1,4 +1,6 @@ 12 + 20 + 16 diff --git a/src/Ursa/Controls/Anchor/Anchor.cs b/src/Ursa/Controls/Anchor/Anchor.cs index 7a75b93..2ecf018 100644 --- a/src/Ursa/Controls/Anchor/Anchor.cs +++ b/src/Ursa/Controls/Anchor/Anchor.cs @@ -11,15 +11,27 @@ using Ursa.Common; namespace Ursa.Controls; /// -/// Some basic assumptions: This should not be a regular SelectingItemsControl, because it does not support multiple selections. -/// Selection should not be exposed to the user, it is only used to determine which item is currently selected. -/// The manipulation of container selection should be simplified. -/// Scroll event of TargetContainer also triggers selection change. +/// Some basic assumptions: This should not be a regular SelectingItemsControl, because it does not support multiple +/// selections. +/// Selection should not be exposed to the user, it is only used to determine which item is currently selected. +/// The manipulation of container selection should be simplified. +/// Scroll event of TargetContainer also triggers selection change. /// -public class Anchor: ItemsControl +public class Anchor : ItemsControl { - public static readonly StyledProperty TargetContainerProperty = AvaloniaProperty.Register( - nameof(TargetContainer)); + public static readonly StyledProperty TargetContainerProperty = + AvaloniaProperty.Register( + nameof(TargetContainer)); + + public static readonly AttachedProperty AnchorIdProperty = + AvaloniaProperty.RegisterAttached("AnchorId"); + + private CancellationTokenSource _cts = new(); + + private List<(string, double)> _positions = []; + private bool _scrollingFromSelection; + + private AnchorItem? _selectedContainer; public ScrollViewer? TargetContainer { @@ -27,12 +39,16 @@ public class Anchor: ItemsControl set => SetValue(TargetContainerProperty, value); } - public static readonly AttachedProperty AnchorIdProperty = - AvaloniaProperty.RegisterAttached("AnchorId"); + public static void SetAnchorId(Visual obj, string? value) + { + obj.SetValue(AnchorIdProperty, value); + } + + public static string? GetAnchorId(Visual obj) + { + return obj.GetValue(AnchorIdProperty); + } - public static void SetAnchorId(Visual obj, string? value) => obj.SetValue(AnchorIdProperty, value); - public static string? GetAnchorId(Visual obj) => obj.GetValue(AnchorIdProperty); - protected override bool NeedsContainerOverride(object? item, int index, out object? recycleKey) { @@ -45,80 +61,65 @@ public class Anchor: ItemsControl return i; } - private CancellationTokenSource _cts = new(); - private bool _scrollingFromSelection; - private void ScrollToAnchor(Visual 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) - { + if (to > TargetContainer.Extent.Height - TargetContainer.Bounds.Height) to = TargetContainer.Extent.Height - TargetContainer.Bounds.Height; - } if (from == to) return; - Animation animation = new Animation() + var animation = new Animation { Duration = TimeSpan.FromSeconds(0.3), Easing = new QuadraticEaseOut(), Children = { - new KeyFrame(){ - Setters = - { - new Setter(ScrollViewer.OffsetProperty, new Vector(0, from)), - }, + new KeyFrame + { + Setters = { new Setter(ScrollViewer.OffsetProperty, new Vector(0, from)) }, Cue = new Cue(0.0) }, - new KeyFrame() + new KeyFrame { - Setters = - { - new Setter(ScrollViewer.OffsetProperty, new Vector(0, to)) - }, + Setters = { new Setter(ScrollViewer.OffsetProperty, new Vector(0, to)) }, Cue = new Cue(1.0) } - } }; _cts.Cancel(); _cts = new CancellationTokenSource(); - var token = _cts.Token; + var token = _cts.Token; token.Register(_ => _scrollingFromSelection = false, null); _scrollingFromSelection = true; animation.RunAsync(TargetContainer, token).ContinueWith(_ => _scrollingFromSelection = false, token); } } - + public void InvalidatePositions() { InvalidateAnchorPositions(); MarkSelectedContainerByPosition(); } - private List<(string, double)> _positions = []; - internal void InvalidateAnchorPositions() { if (TargetContainer is null) return; var items = TargetContainer.GetVisualDescendants().Where(a => GetAnchorId(a) is not null); - List<(string, double)> positions = new List<(string, double)>(); + var positions = new List<(string, double)>(); foreach (var item in items) { var anchorId = GetAnchorId(item); if (anchorId is null) continue; var position = item.TransformToVisual(TargetContainer)?.M32 + TargetContainer.Offset.Y; - if (position.HasValue) - { - positions.Add((anchorId, position.Value)); - } + if (position.HasValue) positions.Add((anchorId, position.Value)); } + positions.Sort((a, b) => a.Item2.CompareTo(b.Item2)); _positions = positions; } @@ -126,14 +127,11 @@ public class Anchor: ItemsControl protected override void OnLoaded(RoutedEventArgs e) { base.OnLoaded(e); - var target = this.TargetContainer; + var target = TargetContainer; if (target is null) return; TargetContainer?.AddHandler(ScrollViewer.ScrollChangedEvent, OnScrollChanged); TargetContainer?.AddHandler(LoadedEvent, OnTargetContainerLoaded); - if (TargetContainer?.IsLoaded == true) - { - InvalidateAnchorPositions(); - } + if (TargetContainer?.IsLoaded == true) InvalidateAnchorPositions(); MarkSelectedContainerByPosition(); } @@ -150,24 +148,24 @@ public class Anchor: ItemsControl if (source is null) return; MarkSelectedContainer(source); var target = TargetContainer?.GetVisualDescendants() - .FirstOrDefault(a => Anchor.GetAnchorId(a) == source.AnchorId); + .FirstOrDefault(a => GetAnchorId(a) == source.AnchorId); if (target is null) return; ScrollToAnchor(target); } /// - /// This method is used to expose the protected CreateContainerForItemOverride method to the AnchorItem class. + /// This method is used to expose the protected CreateContainerForItemOverride method to the AnchorItem class. /// internal Control CreateContainerForItemOverride_INTERNAL(object? item, int index, object? recycleKey) { return CreateContainerForItemOverride(item, index, recycleKey); } - + internal bool NeedsContainerOverride_INTERNAL(object? item, int index, out object? recycleKey) { return NeedsContainerOverride(item, index, out recycleKey); } - + internal void PrepareContainerForItemOverride_INTERNAL(Control container, object? item, int index) { PrepareContainerForItemOverride(container, item, index); @@ -178,8 +176,6 @@ public class Anchor: ItemsControl ContainerForItemPreparedOverride(container, item, index); } - private AnchorItem? _selectedContainer; - internal void MarkSelectedContainer(AnchorItem? item) { var oldValue = _selectedContainer; @@ -197,7 +193,7 @@ public class Anchor: ItemsControl var topAnchorId = _positions.LastOrDefault(a => a.Item2 <= top).Item1; if (topAnchorId is null) return; var item = this.GetVisualDescendants().OfType() - .FirstOrDefault(a => a.AnchorId == topAnchorId); + .FirstOrDefault(a => a.AnchorId == topAnchorId); if (item is null) return; MarkSelectedContainer(item); } diff --git a/src/Ursa/Controls/Anchor/AnchorItem.cs b/src/Ursa/Controls/Anchor/AnchorItem.cs index 6986d6f..17f6917 100644 --- a/src/Ursa/Controls/Anchor/AnchorItem.cs +++ b/src/Ursa/Controls/Anchor/AnchorItem.cs @@ -31,7 +31,7 @@ public class AnchorItem : HeaderedItemsControl, ISelectable { SelectableMixin.Attach(IsSelectedProperty); PressedMixin.Attach(); - ItemsPanelProperty.OverrideDefaultValue(DefaultPanel); + ItemsPanelProperty.OverrideDefaultValue(DefaultPanel); } public int Level From a0554d7add7664ceb7deee6f4e0fdccc18e34a80 Mon Sep 17 00:00:00 2001 From: Dong Bin Date: Tue, 8 Jul 2025 15:11:38 +0800 Subject: [PATCH 09/18] feat: update per review. --- src/Ursa/Common/LogicalHelpers.cs | 4 ++-- src/Ursa/Controls/Anchor/Anchor.cs | 3 ++- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/src/Ursa/Common/LogicalHelpers.cs b/src/Ursa/Common/LogicalHelpers.cs index 4a17487..73b5f17 100644 --- a/src/Ursa/Common/LogicalHelpers.cs +++ b/src/Ursa/Common/LogicalHelpers.cs @@ -5,7 +5,7 @@ namespace Ursa.Common; public static class LogicalHelpers { - public static int CalculateDistanceFromLogicalParent(TItem? item, int @default = -1) + public static int CalculateDistanceFromLogicalParent(TItem? item, int defaultValue = -1) where T : class where TItem : ILogical { @@ -16,6 +16,6 @@ public static class LogicalHelpers if (logical is TItem) result++; logical = logical.LogicalParent; } - return item is not null ? result : @default; + return item is not null ? result : defaultValue; } } \ No newline at end of file diff --git a/src/Ursa/Controls/Anchor/Anchor.cs b/src/Ursa/Controls/Anchor/Anchor.cs index 2ecf018..e9c3566 100644 --- a/src/Ursa/Controls/Anchor/Anchor.cs +++ b/src/Ursa/Controls/Anchor/Anchor.cs @@ -201,7 +201,8 @@ public class Anchor : ItemsControl protected override void OnUnloaded(RoutedEventArgs e) { base.OnUnloaded(e); - TargetContainer?.RemoveHandler(UnloadedEvent, OnTargetContainerLoaded); + TargetContainer?.RemoveHandler(LoadedEvent, OnTargetContainerLoaded); + TargetContainer?.RemoveHandler(ScrollViewer.ScrollChangedEvent, OnScrollChanged); } private void OnTargetContainerLoaded(object? sender, RoutedEventArgs e) From 4f70744dd1a5abe19b83ab874585b783295c891d Mon Sep 17 00:00:00 2001 From: Dong Bin <14807942+rabbitism@users.noreply.github.com> Date: Tue, 8 Jul 2025 15:12:32 +0800 Subject: [PATCH 10/18] feat: update per naming convention Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- src/Ursa/Controls/Anchor/Anchor.cs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/Ursa/Controls/Anchor/Anchor.cs b/src/Ursa/Controls/Anchor/Anchor.cs index e9c3566..4581c3a 100644 --- a/src/Ursa/Controls/Anchor/Anchor.cs +++ b/src/Ursa/Controls/Anchor/Anchor.cs @@ -156,22 +156,22 @@ public class Anchor : ItemsControl /// /// This method is used to expose the protected CreateContainerForItemOverride method to the AnchorItem class. /// - internal Control CreateContainerForItemOverride_INTERNAL(object? item, int index, object? recycleKey) + internal Control CreateContainerForItemOverrideInternal(object? item, int index, object? recycleKey) { return CreateContainerForItemOverride(item, index, recycleKey); } - internal bool NeedsContainerOverride_INTERNAL(object? item, int index, out object? recycleKey) + internal bool NeedsContainerOverrideInternal(object? item, int index, out object? recycleKey) { return NeedsContainerOverride(item, index, out recycleKey); } - internal void PrepareContainerForItemOverride_INTERNAL(Control container, object? item, int index) + internal void PrepareContainerForItemOverrideInternal(Control container, object? item, int index) { PrepareContainerForItemOverride(container, item, index); } - internal void ContainerForItemPreparedOverride_INTERNAL(Control container, object? item, int index) + internal void ContainerForItemPreparedOverrideInternal(Control container, object? item, int index) { ContainerForItemPreparedOverride(container, item, index); } From e8dff305aa0e4b62d287cc1c2784471fc21043e4 Mon Sep 17 00:00:00 2001 From: Dong Bin Date: Tue, 8 Jul 2025 15:14:41 +0800 Subject: [PATCH 11/18] fix: fix caller code. --- src/Ursa/Controls/Anchor/AnchorItem.cs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/Ursa/Controls/Anchor/AnchorItem.cs b/src/Ursa/Controls/Anchor/AnchorItem.cs index 17f6917..6d680e6 100644 --- a/src/Ursa/Controls/Anchor/AnchorItem.cs +++ b/src/Ursa/Controls/Anchor/AnchorItem.cs @@ -66,22 +66,22 @@ public class AnchorItem : HeaderedItemsControl, ISelectable protected override Control CreateContainerForItemOverride(object? item, int index, object? recycleKey) { - return EnsureRoot().CreateContainerForItemOverride_INTERNAL(item, index, recycleKey); + return EnsureRoot().CreateContainerForItemOverrideInternal(item, index, recycleKey); } protected override bool NeedsContainerOverride(object? item, int index, out object? recycleKey) { - return EnsureRoot().NeedsContainerOverride_INTERNAL(item, index, out recycleKey); + return EnsureRoot().NeedsContainerOverrideInternal(item, index, out recycleKey); } protected override void PrepareContainerForItemOverride(Control container, object? item, int index) { - EnsureRoot().PrepareContainerForItemOverride_INTERNAL(container, item, index); + EnsureRoot().PrepareContainerForItemOverrideInternal(container, item, index); } protected override void ContainerForItemPreparedOverride(Control container, object? item, int index) { - EnsureRoot().ContainerForItemPreparedOverride_INTERNAL(container, item, index); + EnsureRoot().ContainerForItemPreparedOverrideInternal(container, item, index); } private Anchor EnsureRoot() From 0323a873d5902a0c1803f304c5799cc8e7ffab6d Mon Sep 17 00:00:00 2001 From: Dong Bin Date: Tue, 8 Jul 2025 15:18:41 +0800 Subject: [PATCH 12/18] feat: add generic constraint to fix null reference issue. --- src/Ursa/Common/VisualHelpers.cs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/Ursa/Common/VisualHelpers.cs b/src/Ursa/Common/VisualHelpers.cs index 37195ac..511a3d2 100644 --- a/src/Ursa/Common/VisualHelpers.cs +++ b/src/Ursa/Common/VisualHelpers.cs @@ -1,13 +1,14 @@ using Avalonia; +using Avalonia.Controls; using Avalonia.VisualTree; namespace Ursa.Common; public static class VisualHelpers { - public static T? GetContainerFromEventSource(this Visual? source) + public static T? GetContainerFromEventSource(this Visual? source) where T: Control { - var item = source.GetSelfAndVisualAncestors().OfType().FirstOrDefault(); + var item = source?.GetSelfAndVisualAncestors().OfType().FirstOrDefault(); return item; } } \ No newline at end of file From 83e3756555edda5d4bca5cbffc7a4a92e30ba7ca Mon Sep 17 00:00:00 2001 From: Dong Bin Date: Tue, 8 Jul 2025 15:24:36 +0800 Subject: [PATCH 13/18] feat: dispose cts. --- src/Ursa/Controls/Anchor/Anchor.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Ursa/Controls/Anchor/Anchor.cs b/src/Ursa/Controls/Anchor/Anchor.cs index 4581c3a..12b600e 100644 --- a/src/Ursa/Controls/Anchor/Anchor.cs +++ b/src/Ursa/Controls/Anchor/Anchor.cs @@ -93,6 +93,7 @@ public class Anchor : ItemsControl } }; _cts.Cancel(); + _cts.Dispose(); _cts = new CancellationTokenSource(); var token = _cts.Token; token.Register(_ => _scrollingFromSelection = false, null); From 8755209fa4d5f528a93c34ba5f7f4fb2fe9472e5 Mon Sep 17 00:00:00 2001 From: Dong Bin <14807942+rabbitism@users.noreply.github.com> Date: Tue, 8 Jul 2025 15:27:41 +0800 Subject: [PATCH 14/18] feat: Clamp thickness. Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- src/Ursa.Themes.Semi/Converters/TreeLevelToMarginConverter.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Ursa.Themes.Semi/Converters/TreeLevelToMarginConverter.cs b/src/Ursa.Themes.Semi/Converters/TreeLevelToMarginConverter.cs index 15ca142..b68433d 100644 --- a/src/Ursa.Themes.Semi/Converters/TreeLevelToMarginConverter.cs +++ b/src/Ursa.Themes.Semi/Converters/TreeLevelToMarginConverter.cs @@ -10,7 +10,7 @@ public class TreeLevelToMarginConverter: IMultiValueConverter { if (values[0] is int i && values[1] is double indent) { - return new Thickness((i-1) * indent, 0, 0, 0); + return new Thickness(Math.Max(i-1, 0) * indent, 0, 0, 0); } return new Thickness(); } From d360ca9ef0a5adb097925ff868d736077b48d649 Mon Sep 17 00:00:00 2001 From: Dong Bin Date: Thu, 10 Jul 2025 11:21:47 +0800 Subject: [PATCH 15/18] feat: rename AnchorId to Id and add TopOffset property. Add headless tests. --- demo/Ursa.Demo/Pages/AnchorDemo.axaml | 34 +++---- src/Ursa/Controls/Anchor/Anchor.cs | 30 ++++--- .../Controls/AnchorTests/TestView.axaml | 90 +++++++++++++++++++ .../Controls/AnchorTests/TestView.axaml.cs | 13 +++ .../Controls/AnchorTests/Tests.cs | 82 +++++++++++++++++ 5 files changed, 221 insertions(+), 28 deletions(-) create mode 100644 tests/HeadlessTest.Ursa/Controls/AnchorTests/TestView.axaml create mode 100644 tests/HeadlessTest.Ursa/Controls/AnchorTests/TestView.axaml.cs create mode 100644 tests/HeadlessTest.Ursa/Controls/AnchorTests/Tests.cs diff --git a/demo/Ursa.Demo/Pages/AnchorDemo.axaml b/demo/Ursa.Demo/Pages/AnchorDemo.axaml index cfcbc10..d74ec28 100644 --- a/demo/Ursa.Demo/Pages/AnchorDemo.axaml +++ b/demo/Ursa.Demo/Pages/AnchorDemo.axaml @@ -25,21 +25,21 @@ @@ -49,24 +49,24 @@ Height="300" HorizontalAlignment="Stretch" Background="{DynamicResource SemiViolet1}"> - + @@ -109,70 +109,70 @@ diff --git a/src/Ursa/Controls/Anchor/Anchor.cs b/src/Ursa/Controls/Anchor/Anchor.cs index 12b600e..f184e8d 100644 --- a/src/Ursa/Controls/Anchor/Anchor.cs +++ b/src/Ursa/Controls/Anchor/Anchor.cs @@ -23,8 +23,8 @@ public class Anchor : ItemsControl AvaloniaProperty.Register( nameof(TargetContainer)); - public static readonly AttachedProperty AnchorIdProperty = - AvaloniaProperty.RegisterAttached("AnchorId"); + public static readonly AttachedProperty IdProperty = + AvaloniaProperty.RegisterAttached("Id"); private CancellationTokenSource _cts = new(); @@ -39,17 +39,25 @@ public class Anchor : ItemsControl set => SetValue(TargetContainerProperty, value); } - public static void SetAnchorId(Visual obj, string? value) + public static void SetId(Visual obj, string? value) { - obj.SetValue(AnchorIdProperty, value); + obj.SetValue(IdProperty, value); } - public static string? GetAnchorId(Visual obj) + public static string? GetId(Visual obj) { - return obj.GetValue(AnchorIdProperty); + return obj.GetValue(IdProperty); } + public static readonly StyledProperty TopOffsetProperty = AvaloniaProperty.Register( + nameof(TopOffset)); + public double TopOffset + { + get => GetValue(TopOffsetProperty); + set => SetValue(TopOffsetProperty, value); + } + protected override bool NeedsContainerOverride(object? item, int index, out object? recycleKey) { return NeedsContainer(item, out recycleKey); @@ -70,7 +78,7 @@ public class Anchor : ItemsControl if (targetPosition.HasValue) { var from = TargetContainer.Offset.Y; - var to = TargetContainer.Offset.Y + targetPosition.Value.Y; + var to = TargetContainer.Offset.Y + targetPosition.Value.Y - TopOffset; if (to > TargetContainer.Extent.Height - TargetContainer.Bounds.Height) to = TargetContainer.Extent.Height - TargetContainer.Bounds.Height; if (from == to) return; @@ -111,11 +119,11 @@ public class Anchor : ItemsControl internal void InvalidateAnchorPositions() { if (TargetContainer is null) return; - var items = TargetContainer.GetVisualDescendants().Where(a => GetAnchorId(a) is not null); + var items = TargetContainer.GetVisualDescendants().Where(a => GetId(a) is not null); var positions = new List<(string, double)>(); foreach (var item in items) { - var anchorId = GetAnchorId(item); + var anchorId = GetId(item); if (anchorId is null) continue; var position = item.TransformToVisual(TargetContainer)?.M32 + TargetContainer.Offset.Y; if (position.HasValue) positions.Add((anchorId, position.Value)); @@ -149,7 +157,7 @@ public class Anchor : ItemsControl if (source is null) return; MarkSelectedContainer(source); var target = TargetContainer?.GetVisualDescendants() - .FirstOrDefault(a => GetAnchorId(a) == source.AnchorId); + .FirstOrDefault(a => GetId(a) == source.AnchorId); if (target is null) return; ScrollToAnchor(target); } @@ -190,7 +198,7 @@ public class Anchor : ItemsControl internal void MarkSelectedContainerByPosition() { if (TargetContainer is null) return; - var top = TargetContainer.Offset.Y; + var top = TargetContainer.Offset.Y + TopOffset; var topAnchorId = _positions.LastOrDefault(a => a.Item2 <= top).Item1; if (topAnchorId is null) return; var item = this.GetVisualDescendants().OfType() diff --git a/tests/HeadlessTest.Ursa/Controls/AnchorTests/TestView.axaml b/tests/HeadlessTest.Ursa/Controls/AnchorTests/TestView.axaml new file mode 100644 index 0000000..271ee63 --- /dev/null +++ b/tests/HeadlessTest.Ursa/Controls/AnchorTests/TestView.axaml @@ -0,0 +1,90 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/tests/HeadlessTest.Ursa/Controls/AnchorTests/TestView.axaml.cs b/tests/HeadlessTest.Ursa/Controls/AnchorTests/TestView.axaml.cs new file mode 100644 index 0000000..80bce48 --- /dev/null +++ b/tests/HeadlessTest.Ursa/Controls/AnchorTests/TestView.axaml.cs @@ -0,0 +1,13 @@ +using Avalonia; +using Avalonia.Controls; +using Avalonia.Markup.Xaml; + +namespace HeadlessTest.Ursa.Controls.AnchorTests; + +public partial class TestView : UserControl +{ + public TestView() + { + InitializeComponent(); + } +} \ No newline at end of file diff --git a/tests/HeadlessTest.Ursa/Controls/AnchorTests/Tests.cs b/tests/HeadlessTest.Ursa/Controls/AnchorTests/Tests.cs new file mode 100644 index 0000000..99a091d --- /dev/null +++ b/tests/HeadlessTest.Ursa/Controls/AnchorTests/Tests.cs @@ -0,0 +1,82 @@ +using Avalonia; +using Avalonia.Controls; +using Avalonia.Headless; +using Avalonia.Headless.XUnit; +using Avalonia.Input; +using Avalonia.Threading; +using Ursa.Controls; + +namespace HeadlessTest.Ursa.Controls.AnchorTests; + +public class Tests +{ + [AvaloniaFact] + public async void Click_Anchor_With_Mouse_Should_Update_Scroll_Offset() + { + var window = new Window() + { + Width = 500, + Height = 500, + }; + var view = new TestView(); + window.Content = view; + window.Show(); + + var anchor = view.FindControl("Anchor"); + var scrollViewer = view.FindControl("ScrollViewer"); + var item4 = view.FindControl("Item4"); + + Assert.NotNull(anchor); + Assert.NotNull(scrollViewer); + Assert.NotNull(item4); + + var transltion = item4.TranslatePoint(new Point(0, 0), window); + + Assert.Equal(0, scrollViewer.Offset.Y); + + // Simulate a click on the anchor + window.MouseDown(new Point(10, transltion.Value.Y+10), MouseButton.Left); + Dispatcher.UIThread.RunJobs(); + await Task.Delay(800); + Assert.True(item4.IsSelected); + Assert.Equal(300.0 * 3, scrollViewer.Offset.Y, 1.0); + } + + [AvaloniaFact] + public async void Change_Scroll_Offset_Should_Update_Selected_Item() + { + var window = new Window() + { + Width = 500, + Height = 500, + }; + var view = new TestView(); + window.Content = view; + window.Show(); + + var anchor = view.FindControl("Anchor"); + var scrollViewer = view.FindControl("ScrollViewer"); + var item1 = view.FindControl("Item1"); + var item2 = view.FindControl("Item2"); + var item4 = view.FindControl("Item4"); + + Assert.NotNull(anchor); + Assert.NotNull(scrollViewer); + Assert.NotNull(item1); + Assert.NotNull(item2); + Assert.NotNull(item4); + + Dispatcher.UIThread.RunJobs(); + + Assert.True(item1.IsSelected); + Assert.False(item2.IsSelected); + + // Change the scroll offset + scrollViewer.Offset = new Vector(0, 310.0); + Dispatcher.UIThread.RunJobs(); + + // Check if the second item is selected + Assert.True(item2.IsSelected); + Assert.False(item4.IsSelected); + } +} \ No newline at end of file From d470488c76150ad6a38e3d42af9ab4671df21dde Mon Sep 17 00:00:00 2001 From: Dong Bin Date: Thu, 10 Jul 2025 11:39:45 +0800 Subject: [PATCH 16/18] test: mvvm tests. --- .../Controls/AnchorTests/TestView2.axaml | 27 +++++++++++ .../Controls/AnchorTests/TestView2.axaml.cs | 46 +++++++++++++++++++ .../Controls/AnchorTests/Tests.cs | 17 ++++++- 3 files changed, 89 insertions(+), 1 deletion(-) create mode 100644 tests/HeadlessTest.Ursa/Controls/AnchorTests/TestView2.axaml create mode 100644 tests/HeadlessTest.Ursa/Controls/AnchorTests/TestView2.axaml.cs diff --git a/tests/HeadlessTest.Ursa/Controls/AnchorTests/TestView2.axaml b/tests/HeadlessTest.Ursa/Controls/AnchorTests/TestView2.axaml new file mode 100644 index 0000000..70cb90c --- /dev/null +++ b/tests/HeadlessTest.Ursa/Controls/AnchorTests/TestView2.axaml @@ -0,0 +1,27 @@ + + + + + + + + + + + + diff --git a/tests/HeadlessTest.Ursa/Controls/AnchorTests/TestView2.axaml.cs b/tests/HeadlessTest.Ursa/Controls/AnchorTests/TestView2.axaml.cs new file mode 100644 index 0000000..23378af --- /dev/null +++ b/tests/HeadlessTest.Ursa/Controls/AnchorTests/TestView2.axaml.cs @@ -0,0 +1,46 @@ +using System.Collections.ObjectModel; +using Avalonia; +using Avalonia.Controls; +using Avalonia.Markup.Xaml; +using CommunityToolkit.Mvvm.ComponentModel; + +namespace HeadlessTest.Ursa.Controls.AnchorTests; + +public partial class TestView2 : UserControl +{ + public TestView2() + { + InitializeComponent(); + DataContext = new AnchorDemoViewModel(); + } +} + +public partial class AnchorDemoViewModel: ObservableObject +{ + public List AnchorItems { get; } = new() + { + new AnchorItemViewModel { AnchorId = "anchor1", Header = "Anchor 1" }, + new AnchorItemViewModel { AnchorId = "anchor2", Header = "Anchor 2" }, + new AnchorItemViewModel + { + AnchorId = "anchor3", Header = "Anchor 3", + Children = + [ + new AnchorItemViewModel() { AnchorId = "anchor3-1", Header = "Anchor 3.1" }, + new AnchorItemViewModel() { AnchorId = "anchor3-2", Header = "Anchor 3.2" }, + new AnchorItemViewModel() { AnchorId = "anchor3-3", Header = "Anchor 3.3" } + ] + }, + new AnchorItemViewModel { AnchorId = "anchor4", Header = "Anchor 4" }, + new AnchorItemViewModel { AnchorId = "anchor5", Header = "Anchor 5" }, + new AnchorItemViewModel { AnchorId = "anchor6", Header = "Anchor 6" }, + new AnchorItemViewModel { AnchorId = "anchor7", Header = "Anchor 7" }, + }; +} + +public partial class AnchorItemViewModel: ObservableObject +{ + [ObservableProperty] private string? _anchorId; + [ObservableProperty] private string? _header; + public ObservableCollection? Children { get; set; } +} \ No newline at end of file diff --git a/tests/HeadlessTest.Ursa/Controls/AnchorTests/Tests.cs b/tests/HeadlessTest.Ursa/Controls/AnchorTests/Tests.cs index 99a091d..2e57d4a 100644 --- a/tests/HeadlessTest.Ursa/Controls/AnchorTests/Tests.cs +++ b/tests/HeadlessTest.Ursa/Controls/AnchorTests/Tests.cs @@ -4,6 +4,7 @@ using Avalonia.Headless; using Avalonia.Headless.XUnit; using Avalonia.Input; using Avalonia.Threading; +using Avalonia.VisualTree; using Ursa.Controls; namespace HeadlessTest.Ursa.Controls.AnchorTests; @@ -39,7 +40,7 @@ public class Tests Dispatcher.UIThread.RunJobs(); await Task.Delay(800); Assert.True(item4.IsSelected); - Assert.Equal(300.0 * 3, scrollViewer.Offset.Y, 1.0); + Assert.Equal(300.0 * 3, scrollViewer.Offset.Y, 50.0); } [AvaloniaFact] @@ -79,4 +80,18 @@ public class Tests Assert.True(item2.IsSelected); Assert.False(item4.IsSelected); } + + [AvaloniaFact] + public void MVVM_Support() + { + var window = new Window(); + var view = new TestView2(); + window.Content = view; + window.Show(); + Dispatcher.UIThread.RunJobs(); + var items = window.GetVisualDescendants().OfType().ToList(); + Assert.NotEmpty(items); + Assert.Equal(10, items.Count); + + } } \ No newline at end of file From b304d585bd599c537c38ab8a359dfec45e3e6a27 Mon Sep 17 00:00:00 2001 From: Zhang Dian <54255897+zdpcdt@users.noreply.github.com> Date: Thu, 10 Jul 2025 19:07:59 +0800 Subject: [PATCH 17/18] feat: enhance Anchor styles. --- demo/Ursa.Demo/Pages/AnchorDemo.axaml | 22 ++++++++++++ .../ViewModels/AnchorDemoViewModel.cs | 15 ++++++-- src/Ursa.Themes.Semi/Controls/Anchor.axaml | 35 +++++++++++-------- src/Ursa.Themes.Semi/Themes/Dark/Anchor.axaml | 8 +++++ src/Ursa.Themes.Semi/Themes/Dark/_index.axaml | 1 + .../Themes/HighContrast/Anchor.axaml | 7 ++++ .../Themes/HighContrast/_index.axaml | 1 + .../Themes/Light/Anchor.axaml | 8 +++++ .../Themes/Light/_index.axaml | 1 + .../Themes/Shared/Anchor.axaml | 4 +-- 10 files changed, 82 insertions(+), 20 deletions(-) create mode 100644 src/Ursa.Themes.Semi/Themes/Dark/Anchor.axaml create mode 100644 src/Ursa.Themes.Semi/Themes/HighContrast/Anchor.axaml create mode 100644 src/Ursa.Themes.Semi/Themes/Light/Anchor.axaml diff --git a/demo/Ursa.Demo/Pages/AnchorDemo.axaml b/demo/Ursa.Demo/Pages/AnchorDemo.axaml index d74ec28..4c6250d 100644 --- a/demo/Ursa.Demo/Pages/AnchorDemo.axaml +++ b/demo/Ursa.Demo/Pages/AnchorDemo.axaml @@ -141,6 +141,27 @@ Background="{DynamicResource SemiPurple1}"> + + + + + + + + + AnchorItems { get; } = new() { @@ -16,7 +16,16 @@ public partial class AnchorDemoViewModel: ObservableObject Children = [ new AnchorItemViewModel() { AnchorId = "anchor3-1", Header = "Anchor 3.1" }, - new AnchorItemViewModel() { AnchorId = "anchor3-2", Header = "Anchor 3.2" }, + new AnchorItemViewModel() + { + AnchorId = "anchor3-2", Header = "Anchor 3.2", + Children = + [ + new AnchorItemViewModel() { AnchorId = "anchor3-2-1", Header = "Anchor 3.2.1" }, + new AnchorItemViewModel() { AnchorId = "anchor3-2-2", Header = "Anchor 3.2.2" }, + new AnchorItemViewModel() { AnchorId = "anchor3-2-3", Header = "Anchor 3.2.3" } + ] + }, new AnchorItemViewModel() { AnchorId = "anchor3-3", Header = "Anchor 3.3" } ] }, @@ -27,7 +36,7 @@ public partial class AnchorDemoViewModel: ObservableObject }; } -public partial class AnchorItemViewModel: ObservableObject +public partial class AnchorItemViewModel : ObservableObject { [ObservableProperty] private string? _anchorId; [ObservableProperty] private string? _header; diff --git a/src/Ursa.Themes.Semi/Controls/Anchor.axaml b/src/Ursa.Themes.Semi/Controls/Anchor.axaml index 108f37b..63a897e 100644 --- a/src/Ursa.Themes.Semi/Controls/Anchor.axaml +++ b/src/Ursa.Themes.Semi/Controls/Anchor.axaml @@ -16,7 +16,7 @@ Name="PART_Pipe" HorizontalAlignment="Left" VerticalAlignment="Stretch" - Fill="{DynamicResource SemiColorBorder}" /> + Fill="{DynamicResource AnchorPipeBackground}" /> - + @@ -68,19 +68,24 @@ - - - - + + + + diff --git a/src/Ursa.Themes.Semi/Themes/Dark/Anchor.axaml b/src/Ursa.Themes.Semi/Themes/Dark/Anchor.axaml new file mode 100644 index 0000000..a3e1e55 --- /dev/null +++ b/src/Ursa.Themes.Semi/Themes/Dark/Anchor.axaml @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/src/Ursa.Themes.Semi/Themes/Dark/_index.axaml b/src/Ursa.Themes.Semi/Themes/Dark/_index.axaml index 0d5148a..8a8a444 100644 --- a/src/Ursa.Themes.Semi/Themes/Dark/_index.axaml +++ b/src/Ursa.Themes.Semi/Themes/Dark/_index.axaml @@ -1,6 +1,7 @@ + diff --git a/src/Ursa.Themes.Semi/Themes/HighContrast/Anchor.axaml b/src/Ursa.Themes.Semi/Themes/HighContrast/Anchor.axaml new file mode 100644 index 0000000..f79130a --- /dev/null +++ b/src/Ursa.Themes.Semi/Themes/HighContrast/Anchor.axaml @@ -0,0 +1,7 @@ + + + + + + + \ No newline at end of file diff --git a/src/Ursa.Themes.Semi/Themes/HighContrast/_index.axaml b/src/Ursa.Themes.Semi/Themes/HighContrast/_index.axaml index 2014115..bdde3cf 100644 --- a/src/Ursa.Themes.Semi/Themes/HighContrast/_index.axaml +++ b/src/Ursa.Themes.Semi/Themes/HighContrast/_index.axaml @@ -1,5 +1,6 @@ + diff --git a/src/Ursa.Themes.Semi/Themes/Light/Anchor.axaml b/src/Ursa.Themes.Semi/Themes/Light/Anchor.axaml new file mode 100644 index 0000000..a3e1e55 --- /dev/null +++ b/src/Ursa.Themes.Semi/Themes/Light/Anchor.axaml @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/src/Ursa.Themes.Semi/Themes/Light/_index.axaml b/src/Ursa.Themes.Semi/Themes/Light/_index.axaml index 0d5148a..8a8a444 100644 --- a/src/Ursa.Themes.Semi/Themes/Light/_index.axaml +++ b/src/Ursa.Themes.Semi/Themes/Light/_index.axaml @@ -1,6 +1,7 @@ + diff --git a/src/Ursa.Themes.Semi/Themes/Shared/Anchor.axaml b/src/Ursa.Themes.Semi/Themes/Shared/Anchor.axaml index cffb17e..770573b 100644 --- a/src/Ursa.Themes.Semi/Themes/Shared/Anchor.axaml +++ b/src/Ursa.Themes.Semi/Themes/Shared/Anchor.axaml @@ -1,6 +1,6 @@ - + 12 20 16 + From 3ae76efd45c6092d6a918638ff11f70b8c000598 Mon Sep 17 00:00:00 2001 From: Zhang Dian <54255897+zdpcdt@users.noreply.github.com> Date: Thu, 10 Jul 2025 19:55:34 +0800 Subject: [PATCH 18/18] feat: update AnchorItem Padding Converter. --- demo/Ursa.Demo/Pages/AnchorDemo.axaml | 54 ++++++++++++++++--- src/Ursa.Themes.Semi/Controls/Anchor.axaml | 20 +++---- ...rter.cs => TreeLevelToPaddingConverter.cs} | 7 +-- .../Themes/Shared/Anchor.axaml | 8 +-- 4 files changed, 67 insertions(+), 22 deletions(-) rename src/Ursa.Themes.Semi/Converters/{TreeLevelToMarginConverter.cs => TreeLevelToPaddingConverter.cs} (55%) diff --git a/demo/Ursa.Demo/Pages/AnchorDemo.axaml b/demo/Ursa.Demo/Pages/AnchorDemo.axaml index 4c6250d..9f4c56b 100644 --- a/demo/Ursa.Demo/Pages/AnchorDemo.axaml +++ b/demo/Ursa.Demo/Pages/AnchorDemo.axaml @@ -79,17 +79,15 @@ Width="200" Margin="24" TargetContainer="{Binding ElementName=container1}"> - + - + - - @@ -201,7 +199,6 @@ - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/Ursa.Themes.Semi/Controls/Anchor.axaml b/src/Ursa.Themes.Semi/Controls/Anchor.axaml index 63a897e..87d292e 100644 --- a/src/Ursa.Themes.Semi/Controls/Anchor.axaml +++ b/src/Ursa.Themes.Semi/Controls/Anchor.axaml @@ -4,7 +4,7 @@ xmlns:converters="clr-namespace:Ursa.Themes.Semi.Converters" xmlns:iri="https://irihi.tech/shared" xmlns:u="https://irihi.tech/ursa"> - + @@ -12,7 +12,7 @@ - @@ -39,23 +39,23 @@ - + - - + + - + @@ -83,7 +83,7 @@ - diff --git a/src/Ursa.Themes.Semi/Converters/TreeLevelToMarginConverter.cs b/src/Ursa.Themes.Semi/Converters/TreeLevelToPaddingConverter.cs similarity index 55% rename from src/Ursa.Themes.Semi/Converters/TreeLevelToMarginConverter.cs rename to src/Ursa.Themes.Semi/Converters/TreeLevelToPaddingConverter.cs index b68433d..e99e9ea 100644 --- a/src/Ursa.Themes.Semi/Converters/TreeLevelToMarginConverter.cs +++ b/src/Ursa.Themes.Semi/Converters/TreeLevelToPaddingConverter.cs @@ -4,14 +4,15 @@ using Avalonia.Data.Converters; namespace Ursa.Themes.Semi.Converters; -public class TreeLevelToMarginConverter: IMultiValueConverter +public class TreeLevelToPaddingConverter : IMultiValueConverter { public object? Convert(IList values, Type targetType, object? parameter, CultureInfo culture) { - if (values[0] is int i && values[1] is double indent) + if (values[0] is int i && values[1] is Thickness indent) { - return new Thickness(Math.Max(i-1, 0) * indent, 0, 0, 0); + return new Thickness(Math.Max(i, 0) * indent.Left, indent.Top, indent.Right, indent.Bottom); } + return new Thickness(); } } \ No newline at end of file diff --git a/src/Ursa.Themes.Semi/Themes/Shared/Anchor.axaml b/src/Ursa.Themes.Semi/Themes/Shared/Anchor.axaml index 770573b..1f56148 100644 --- a/src/Ursa.Themes.Semi/Themes/Shared/Anchor.axaml +++ b/src/Ursa.Themes.Semi/Themes/Shared/Anchor.axaml @@ -1,6 +1,8 @@ - 12 + 8,4,0,4 + 2 + 1 20 16 - - + 12 + \ No newline at end of file