feat: WIP.

This commit is contained in:
Dong Bin
2025-07-03 23:06:22 +08:00
parent 6deccdc1ac
commit 817eb9acc9
5 changed files with 135 additions and 59 deletions

View File

@@ -42,8 +42,8 @@ EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "GitHub Action", "GitHub Action", "{66123AC1-7C8C-4AA0-BBDB-5CC3E647A741}" Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "GitHub Action", "GitHub Action", "{66123AC1-7C8C-4AA0-BBDB-5CC3E647A741}"
ProjectSection(SolutionItems) = preProject ProjectSection(SolutionItems) = preProject
.github\workflows\deploy.yml = .github\workflows\deploy.yml .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-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\publish.yml = .github\workflows\publish.yml
.github\workflows\release-tag.yml = .github\workflows\release-tag.yml .github\workflows\release-tag.yml = .github\workflows\release-tag.yml
.github\workflows\test.yml = .github\workflows\test.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 {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.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.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.ActiveCfg = Release|Any CPU
{3FC76CD9-CE5D-4804-A8D6-4E292EB296AA}.Release|Any CPU.Build.0 = 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 {B6BAB821-A9FE-44F3-B9CD-06E27FDB63F6}.Debug|Any CPU.ActiveCfg = Debug|Any CPU

View File

@@ -25,49 +25,48 @@
</Style> </Style>
</StackPanel.Styles> </StackPanel.Styles>
<Border <Border
Name="a1" u:Anchor.AnchorId="a1"
Height="300" Height="300"
HorizontalAlignment="Stretch" HorizontalAlignment="Stretch"
Background="{DynamicResource SemiRed2}"> Background="{DynamicResource SemiRed2}">
<TextBlock Text="Border 1" /> <TextBlock Text="Border 1" />
</Border> </Border>
<Border <Border
Name="a2" u:Anchor.AnchorId="a2"
Height="300" Height="300"
HorizontalAlignment="Stretch" HorizontalAlignment="Stretch"
Background="{DynamicResource SemiPink1}"> Background="{DynamicResource SemiPink1}">
<TextBlock Text="Border 2" /> <TextBlock Text="Border 2" />
</Border> </Border>
<Border <Border
Name="a3" u:Anchor.AnchorId="a3"
Height="300" Height="300"
HorizontalAlignment="Stretch" HorizontalAlignment="Stretch"
Background="{DynamicResource SemiPurple1}"> Background="{DynamicResource SemiPurple1}">
<TextBlock Text="Border 3" /> <TextBlock Text="Border 3" />
</Border> </Border>
<Border <Border
Name="a4"
Height="300" Height="300"
HorizontalAlignment="Stretch" HorizontalAlignment="Stretch"
Background="{DynamicResource SemiViolet1}"> Background="{DynamicResource SemiViolet1}">
<TextBlock Text="Border 4" /> <TextBlock u:Anchor.AnchorId="a4" Text="Border 4" />
</Border> </Border>
<Border <Border
Name="a5" u:Anchor.AnchorId="a5"
Height="300" Height="300"
HorizontalAlignment="Stretch" HorizontalAlignment="Stretch"
Background="{DynamicResource SemiIndigo1}"> Background="{DynamicResource SemiIndigo1}">
<TextBlock Text="Border 5" /> <TextBlock Text="Border 5" />
</Border> </Border>
<Border <Border
Name="a6" u:Anchor.AnchorId="a6"
Height="300" Height="300"
HorizontalAlignment="Stretch" HorizontalAlignment="Stretch"
Background="{DynamicResource SemiBlue1}"> Background="{DynamicResource SemiBlue1}">
<TextBlock Text="Border 6" /> <TextBlock Text="Border 6" />
</Border> </Border>
<Border <Border
Name="a7" u:Anchor.AnchorId="a7"
Height="300" Height="300"
HorizontalAlignment="Stretch" HorizontalAlignment="Stretch"
Background="{DynamicResource SemiLightBlue1}"> Background="{DynamicResource SemiLightBlue1}">
@@ -80,13 +79,16 @@
Width="200" Width="200"
Margin="24" Margin="24"
TargetContainer="{Binding ElementName=container1}"> TargetContainer="{Binding ElementName=container1}">
<u:AnchorItem Content="Rectangle 1" Target="{Binding #a1}" /> <u:AnchorItem Header="Rectangle 1" AnchorId="a1" >
<u:AnchorItem Content="Rectangle 2" Target="{Binding #a2}" /> <u:AnchorItem Header="Rectangle 2" AnchorId="a2" />
<u:AnchorItem Content="Rectangle 3" Target="{Binding #a3}" /> <u:AnchorItem Header="Rectangle 3" AnchorId="a3" />
<u:AnchorItem Content="Rectangle 4" Target="{Binding #a4}" /> </u:AnchorItem>
<u:AnchorItem Content="Rectangle 5" Target="{Binding #a5}" /> <u:AnchorItem Header="Rectangle 4" AnchorId="a4" />
<u:AnchorItem Content="Rectangle 6" Target="{Binding #a6}" /> <u:AnchorItem Header="Rectangle 5" AnchorId="a5" >
<u:AnchorItem Content="Rectangle 7" Target="{Binding #a7}" /> <u:AnchorItem Header="Rectangle 6" AnchorId="a6" />
<u:AnchorItem Header="Rectangle 7" AnchorId="a7" />
</u:AnchorItem>
</u:Anchor> </u:Anchor>
</Grid> </Grid>
@@ -157,13 +159,14 @@
</ScrollViewer> </ScrollViewer>
<u:Anchor <u:Anchor
Grid.Column="1" Grid.Column="1"
MinWidth="300" Width="200"
Margin="24"
ItemsSource="{Binding AnchorItems}" ItemsSource="{Binding AnchorItems}"
TargetContainer="{Binding #container2}"> TargetContainer="{Binding #container2}">
<u:Anchor.Styles> <u:Anchor.Styles>
<Style x:DataType="viewModels:AnchorItemViewModel" Selector="u|AnchorItem"> <Style x:DataType="viewModels:AnchorItemViewModel" Selector="u|AnchorItem">
<Setter Property="AnchorId" Value="{Binding AnchorId}" /> <Setter Property="AnchorId" Value="{Binding AnchorId}" />
<Setter Property="Content" Value="{Binding Header}" /> <Setter Property="Header" Value="{Binding Header}" />
</Style> </Style>
</u:Anchor.Styles> </u:Anchor.Styles>
</u:Anchor> </u:Anchor>

View File

@@ -12,4 +12,16 @@
</ControlTemplate> </ControlTemplate>
</Setter> </Setter>
</ControlTheme> </ControlTheme>
<ControlTheme x:Key="{x:Type u:AnchorItem}" TargetType="u:AnchorItem" >
<Setter Property="Background" Value="Transparent" />
<Setter Property="Template">
<ControlTemplate TargetType="u:AnchorItem">
<StackPanel>
<ContentPresenter Content="{TemplateBinding Header}" />
<ItemsPresenter Margin="8 0 0 0" ItemsPanel="{TemplateBinding ItemsPanel}" />
</StackPanel>
</ControlTemplate>
</Setter>
</ControlTheme>
</ResourceDictionary> </ResourceDictionary>

View File

@@ -47,11 +47,12 @@ public class Anchor: SelectingItemsControl
ScrollToAnchor(target); ScrollToAnchor(target);
} }
internal void ScrollToAnchor(Visual target) private CancellationTokenSource _cts = new();
private void ScrollToAnchor(Visual target)
{ {
if (TargetContainer is null) if (TargetContainer is null)
return; return;
TargetContainer.Loaded += OnTargetLoaded;
var targetPosition = target.TranslatePoint(new Point(0, 0), TargetContainer); var targetPosition = target.TranslatePoint(new Point(0, 0), TargetContainer);
if (targetPosition.HasValue) if (targetPosition.HasValue)
{ {
@@ -61,6 +62,7 @@ public class Anchor: SelectingItemsControl
{ {
to = TargetContainer.Extent.Height - TargetContainer.Bounds.Height; to = TargetContainer.Extent.Height - TargetContainer.Bounds.Height;
} }
if (from == to) return;
Animation animation = new Animation() Animation animation = new Animation()
{ {
Duration = TimeSpan.FromSeconds(0.3), Duration = TimeSpan.FromSeconds(0.3),
@@ -85,24 +87,9 @@ public class Anchor: SelectingItemsControl
} }
}; };
animation.RunAsync(TargetContainer); _cts.Cancel();
// TargetContainer.Offset = TargetContainer.Offset.WithY(TargetContainer.Offset.Y + targetPosition.Value.Y); _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);
}
}
} }
} }
@@ -110,4 +97,41 @@ public class Anchor: SelectingItemsControl
{ {
} }
protected override void OnLoaded(RoutedEventArgs e)
{
base.OnLoaded(e);
var items = this.GetVisualDescendants().OfType<AnchorItem>().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);
}
} }

View File

@@ -1,51 +1,87 @@
using System.Windows.Input;
using Avalonia; using Avalonia;
using Avalonia.Controls; using Avalonia.Controls;
using Avalonia.Controls.Mixins;
using Avalonia.Controls.Primitives;
using Avalonia.Controls.Templates;
using Avalonia.Input; using Avalonia.Input;
using Avalonia.VisualTree; using Avalonia.LogicalTree;
namespace Ursa.Controls; namespace Ursa.Controls;
public class AnchorItem: ContentControl public class AnchorItem : HeaderedItemsControl, ISelectable
{ {
public static readonly StyledProperty<Control?> TargetProperty = AvaloniaProperty.Register<AnchorItem, Control?>(
nameof(Target));
public Control? Target
{
get => GetValue(TargetProperty);
set => SetValue(TargetProperty, value);
}
public static readonly StyledProperty<string?> AnchorIdProperty = AvaloniaProperty.Register<AnchorItem, string?>( public static readonly StyledProperty<string?> AnchorIdProperty = AvaloniaProperty.Register<AnchorItem, string?>(
nameof(AnchorId)); nameof(AnchorId));
public static readonly StyledProperty<bool> IsSelectedProperty =
SelectingItemsControl.IsSelectedProperty.AddOwner<AnchorItem>();
private static readonly FuncTemplate<Panel?> DefaultPanel =
new(() => new StackPanel());
private Anchor? _root;
static AnchorItem()
{
SelectableMixin.Attach<AnchorItem>(IsSelectedProperty);
PressedMixin.Attach<AnchorItem>();
ItemsPanelProperty.OverrideDefaultValue<TreeViewItem>(DefaultPanel);
}
public string? AnchorId public string? AnchorId
{ {
get => GetValue(AnchorIdProperty); get => GetValue(AnchorIdProperty);
set => SetValue(AnchorIdProperty, value); set => SetValue(AnchorIdProperty, value);
} }
private Anchor? _root; public bool IsSelected
{
get => GetValue(IsSelectedProperty);
set => SetValue(IsSelectedProperty, value);
}
protected override void OnAttachedToVisualTree(VisualTreeAttachmentEventArgs e) protected override void OnAttachedToVisualTree(VisualTreeAttachmentEventArgs e)
{ {
base.OnAttachedToVisualTree(e); base.OnAttachedToVisualTree(e);
_root = this.FindAncestorOfType<Anchor>() ?? _root = this.GetLogicalAncestors().OfType<Anchor>().FirstOrDefault();
throw new InvalidOperationException("AnchorItem must be inside an Anchor control.");
} }
protected override void OnPointerPressed(PointerPressedEventArgs e) protected override void OnPointerPressed(PointerPressedEventArgs e)
{ {
var item = new TreeViewItem();
base.OnPointerPressed(e); base.OnPointerPressed(e);
if (e.Handled) return;
if (_root is null) if (_root is null)
return; return;
if (Target is not null) if (!string.IsNullOrEmpty(AnchorId))
{
_root.ScrollToAnchor(Target);
}
else if (!string.IsNullOrEmpty(AnchorId))
{ {
_root.ScrollToAnchor(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.");
}
} }