3
Ursa.sln
3
Ursa.sln
@@ -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
|
||||||
|
|||||||
265
demo/Ursa.Demo/Pages/AnchorDemo.axaml
Normal file
265
demo/Ursa.Demo/Pages/AnchorDemo.axaml
Normal file
@@ -0,0 +1,265 @@
|
|||||||
|
<UserControl
|
||||||
|
x:Class="Ursa.Demo.Pages.AnchorDemo"
|
||||||
|
xmlns="https://github.com/avaloniaui"
|
||||||
|
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||||
|
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">
|
||||||
|
<TabControl>
|
||||||
|
<TabItem Header="XAML Inline">
|
||||||
|
<Grid ColumnDefinitions="*, Auto">
|
||||||
|
<ScrollViewer
|
||||||
|
Name="container1"
|
||||||
|
Grid.Column="0"
|
||||||
|
VerticalAlignment="Stretch">
|
||||||
|
<StackPanel>
|
||||||
|
<StackPanel.Styles>
|
||||||
|
<Style Selector="TextBlock">
|
||||||
|
<Setter Property="HorizontalAlignment" Value="Center" />
|
||||||
|
<Setter Property="VerticalAlignment" Value="Center" />
|
||||||
|
</Style>
|
||||||
|
</StackPanel.Styles>
|
||||||
|
<Border
|
||||||
|
u:Anchor.Id="a1"
|
||||||
|
Height="300"
|
||||||
|
HorizontalAlignment="Stretch"
|
||||||
|
Background="{DynamicResource SemiRed2}">
|
||||||
|
<TextBlock Text="Border 1" />
|
||||||
|
</Border>
|
||||||
|
<Border
|
||||||
|
u:Anchor.Id="a2"
|
||||||
|
Height="300"
|
||||||
|
HorizontalAlignment="Stretch"
|
||||||
|
Background="{DynamicResource SemiPink1}">
|
||||||
|
<TextBlock Text="Border 2" />
|
||||||
|
</Border>
|
||||||
|
<Border
|
||||||
|
u:Anchor.Id="a3"
|
||||||
|
Height="300"
|
||||||
|
HorizontalAlignment="Stretch"
|
||||||
|
Background="{DynamicResource SemiPurple1}">
|
||||||
|
<TextBlock Text="Border 3" />
|
||||||
|
</Border>
|
||||||
|
<Border
|
||||||
|
Height="300"
|
||||||
|
HorizontalAlignment="Stretch"
|
||||||
|
Background="{DynamicResource SemiViolet1}">
|
||||||
|
<TextBlock u:Anchor.Id="a4" Text="Border 4" />
|
||||||
|
</Border>
|
||||||
|
<Border
|
||||||
|
u:Anchor.Id="a5"
|
||||||
|
Height="300"
|
||||||
|
HorizontalAlignment="Stretch"
|
||||||
|
Background="{DynamicResource SemiIndigo1}">
|
||||||
|
<TextBlock Text="Border 5" />
|
||||||
|
</Border>
|
||||||
|
<Border
|
||||||
|
u:Anchor.Id="a6"
|
||||||
|
Height="300"
|
||||||
|
HorizontalAlignment="Stretch"
|
||||||
|
Background="{DynamicResource SemiBlue1}">
|
||||||
|
<TextBlock Text="Border 6" />
|
||||||
|
</Border>
|
||||||
|
<Border
|
||||||
|
u:Anchor.Id="a7"
|
||||||
|
Height="300"
|
||||||
|
HorizontalAlignment="Stretch"
|
||||||
|
Background="{DynamicResource SemiLightBlue1}">
|
||||||
|
<TextBlock Text="Border 7" />
|
||||||
|
</Border>
|
||||||
|
</StackPanel>
|
||||||
|
</ScrollViewer>
|
||||||
|
<u:Anchor
|
||||||
|
Grid.Column="1"
|
||||||
|
Width="200"
|
||||||
|
Margin="24"
|
||||||
|
TargetContainer="{Binding ElementName=container1}">
|
||||||
|
<u:AnchorItem Header="Rectangle 1" AnchorId="a1">
|
||||||
|
<u:AnchorItem Header="Rectangle 2" AnchorId="a2" />
|
||||||
|
<u:AnchorItem Header="Rectangle 3" AnchorId="a3" />
|
||||||
|
</u:AnchorItem>
|
||||||
|
<u:AnchorItem Header="Rectangle 4" AnchorId="a4" />
|
||||||
|
<u:AnchorItem Header="Rectangle 5" AnchorId="a5">
|
||||||
|
<u:AnchorItem Header="Rectangle 6" AnchorId="a6" />
|
||||||
|
<u:AnchorItem Header="Rectangle 7" AnchorId="a7" />
|
||||||
|
</u:AnchorItem>
|
||||||
|
</u:Anchor>
|
||||||
|
</Grid>
|
||||||
|
</TabItem>
|
||||||
|
<TabItem Header="MVVM">
|
||||||
|
<Grid ColumnDefinitions="*, Auto">
|
||||||
|
<ScrollViewer
|
||||||
|
Name="container2"
|
||||||
|
Grid.Column="0"
|
||||||
|
VerticalAlignment="Stretch">
|
||||||
|
<StackPanel>
|
||||||
|
<StackPanel.Styles>
|
||||||
|
<Style Selector="TextBlock">
|
||||||
|
<Setter Property="HorizontalAlignment" Value="Center" />
|
||||||
|
<Setter Property="VerticalAlignment" Value="Center" />
|
||||||
|
</Style>
|
||||||
|
</StackPanel.Styles>
|
||||||
|
<Border
|
||||||
|
Height="300"
|
||||||
|
HorizontalAlignment="Stretch"
|
||||||
|
u:Anchor.Id="anchor1"
|
||||||
|
Background="{DynamicResource SemiRed2}">
|
||||||
|
<TextBlock Text="Border 1" />
|
||||||
|
</Border>
|
||||||
|
<Border
|
||||||
|
Height="300"
|
||||||
|
HorizontalAlignment="Stretch"
|
||||||
|
u:Anchor.Id="anchor2"
|
||||||
|
Background="{DynamicResource SemiPink1}">
|
||||||
|
<TextBlock Text="Border 2" />
|
||||||
|
</Border>
|
||||||
|
<Border
|
||||||
|
Height="300"
|
||||||
|
HorizontalAlignment="Stretch"
|
||||||
|
u:Anchor.Id="anchor3"
|
||||||
|
Background="{DynamicResource SemiPurple1}">
|
||||||
|
<TextBlock Text="Border 3" />
|
||||||
|
</Border>
|
||||||
|
<Border
|
||||||
|
Height="300"
|
||||||
|
HorizontalAlignment="Stretch"
|
||||||
|
u:Anchor.Id="anchor3-1"
|
||||||
|
Background="{DynamicResource SemiPurple1}">
|
||||||
|
<TextBlock Text="Border 3-1" />
|
||||||
|
</Border>
|
||||||
|
<Border
|
||||||
|
Height="300"
|
||||||
|
HorizontalAlignment="Stretch"
|
||||||
|
u:Anchor.Id="anchor3-2"
|
||||||
|
Background="{DynamicResource SemiPurple1}">
|
||||||
|
<TextBlock Text="Border 3-2" />
|
||||||
|
</Border>
|
||||||
|
<Border
|
||||||
|
Height="300"
|
||||||
|
HorizontalAlignment="Stretch"
|
||||||
|
u:Anchor.Id="anchor3-2-1"
|
||||||
|
Background="{DynamicResource SemiCyan1}">
|
||||||
|
<TextBlock Text="Border 3-2-1" />
|
||||||
|
</Border>
|
||||||
|
<Border
|
||||||
|
Height="300"
|
||||||
|
HorizontalAlignment="Stretch"
|
||||||
|
u:Anchor.Id="anchor3-2-2"
|
||||||
|
Background="{DynamicResource SemiCyan1}">
|
||||||
|
<TextBlock Text="Border 3-2-2" />
|
||||||
|
</Border>
|
||||||
|
<Border
|
||||||
|
Height="300"
|
||||||
|
HorizontalAlignment="Stretch"
|
||||||
|
u:Anchor.Id="anchor3-2-3"
|
||||||
|
Background="{DynamicResource SemiCyan1}">
|
||||||
|
<TextBlock Text="Border 3-2-3" />
|
||||||
|
</Border>
|
||||||
|
<Border
|
||||||
|
Height="300"
|
||||||
|
HorizontalAlignment="Stretch"
|
||||||
|
u:Anchor.Id="anchor3-3"
|
||||||
|
Background="{DynamicResource SemiPurple1}">
|
||||||
|
<TextBlock Text="Border 3-3" />
|
||||||
|
</Border>
|
||||||
|
<Border
|
||||||
|
Height="300"
|
||||||
|
HorizontalAlignment="Stretch"
|
||||||
|
u:Anchor.Id="anchor4"
|
||||||
|
Background="{DynamicResource SemiViolet1}">
|
||||||
|
<TextBlock Text="Border 4" />
|
||||||
|
</Border>
|
||||||
|
<Border
|
||||||
|
Height="300"
|
||||||
|
HorizontalAlignment="Stretch"
|
||||||
|
u:Anchor.Id="anchor5"
|
||||||
|
Background="{DynamicResource SemiIndigo1}">
|
||||||
|
<TextBlock Text="Border 5" />
|
||||||
|
</Border>
|
||||||
|
<Border
|
||||||
|
Height="300"
|
||||||
|
HorizontalAlignment="Stretch"
|
||||||
|
u:Anchor.Id="anchor6"
|
||||||
|
Background="{DynamicResource SemiBlue1}">
|
||||||
|
<TextBlock Text="Border 6" />
|
||||||
|
</Border>
|
||||||
|
<Border
|
||||||
|
Height="300"
|
||||||
|
HorizontalAlignment="Stretch"
|
||||||
|
u:Anchor.Id="anchor7"
|
||||||
|
Background="{DynamicResource SemiLightBlue1}">
|
||||||
|
<TextBlock Text="Border 7" />
|
||||||
|
</Border>
|
||||||
|
</StackPanel>
|
||||||
|
</ScrollViewer>
|
||||||
|
<u:Anchor
|
||||||
|
Grid.Column="1"
|
||||||
|
Width="200"
|
||||||
|
Margin="24"
|
||||||
|
ItemsSource="{Binding AnchorItems}"
|
||||||
|
TargetContainer="{Binding #container2}">
|
||||||
|
<u:Anchor.Styles>
|
||||||
|
<Style x:DataType="viewModels:AnchorItemViewModel" Selector="u|AnchorItem">
|
||||||
|
<Setter Property="AnchorId" Value="{Binding AnchorId}" />
|
||||||
|
</Style>
|
||||||
|
</u:Anchor.Styles>
|
||||||
|
<u:Anchor.ItemTemplate>
|
||||||
|
<TreeDataTemplate ItemsSource="{Binding Children}">
|
||||||
|
<TextBlock Text="{Binding Header}" />
|
||||||
|
</TreeDataTemplate>
|
||||||
|
</u:Anchor.ItemTemplate>
|
||||||
|
</u:Anchor>
|
||||||
|
</Grid>
|
||||||
|
</TabItem>
|
||||||
|
<TabItem Header="Appearance">
|
||||||
|
<StackPanel
|
||||||
|
Orientation="Horizontal"
|
||||||
|
Spacing="100"
|
||||||
|
Margin="8">
|
||||||
|
<u:Anchor>
|
||||||
|
<u:AnchorItem Header="组件" />
|
||||||
|
<u:AnchorItem Header="设计语言" />
|
||||||
|
<u:AnchorItem Header="物料平台" />
|
||||||
|
<u:AnchorItem Header="主题商店" />
|
||||||
|
</u:Anchor>
|
||||||
|
<u:Anchor Classes="Small">
|
||||||
|
<u:AnchorItem Header="组件" />
|
||||||
|
<u:AnchorItem Header="设计语言" />
|
||||||
|
<u:AnchorItem Header="物料平台" />
|
||||||
|
<u:AnchorItem Header="主题商店" />
|
||||||
|
</u:Anchor>
|
||||||
|
<u:Anchor Classes="Tertiary">
|
||||||
|
<u:AnchorItem Header="尺寸" />
|
||||||
|
<u:AnchorItem Header="组件" />
|
||||||
|
<u:AnchorItem Header="设计语言" />
|
||||||
|
<u:AnchorItem Header="物料平台" />
|
||||||
|
<u:AnchorItem Header="主题商店" />
|
||||||
|
</u:Anchor>
|
||||||
|
<u:Anchor Classes="Muted">
|
||||||
|
<u:AnchorItem Header="尺寸" />
|
||||||
|
<u:AnchorItem Header="组件" />
|
||||||
|
<u:AnchorItem Header="设计语言" />
|
||||||
|
<u:AnchorItem Header="物料平台" />
|
||||||
|
<u:AnchorItem Header="主题商店" />
|
||||||
|
</u:Anchor>
|
||||||
|
<u:Anchor>
|
||||||
|
<u:AnchorItem Header="1. 动态展示">
|
||||||
|
<u:AnchorItem Header="1.1 组件">
|
||||||
|
<u:AnchorItem Header="1.1.1 Avatar" />
|
||||||
|
<u:AnchorItem Header="1.1.2 Button" />
|
||||||
|
<u:AnchorItem Header="1.1.3 Icon" />
|
||||||
|
</u:AnchorItem>
|
||||||
|
<u:AnchorItem Header="1.2 物料" />
|
||||||
|
<u:AnchorItem Header="1.3 主题商店" />
|
||||||
|
</u:AnchorItem>
|
||||||
|
<u:AnchorItem Header="2. 设计语言" />
|
||||||
|
</u:Anchor>
|
||||||
|
</StackPanel>
|
||||||
|
</TabItem>
|
||||||
|
</TabControl>
|
||||||
|
</UserControl>
|
||||||
13
demo/Ursa.Demo/Pages/AnchorDemo.axaml.cs
Normal file
13
demo/Ursa.Demo/Pages/AnchorDemo.axaml.cs
Normal file
@@ -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();
|
||||||
|
}
|
||||||
|
}
|
||||||
44
demo/Ursa.Demo/ViewModels/AnchorDemoViewModel.cs
Normal file
44
demo/Ursa.Demo/ViewModels/AnchorDemoViewModel.cs
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Collections.ObjectModel;
|
||||||
|
using CommunityToolkit.Mvvm.ComponentModel;
|
||||||
|
|
||||||
|
namespace Ursa.Demo.ViewModels;
|
||||||
|
|
||||||
|
public partial class AnchorDemoViewModel : ObservableObject
|
||||||
|
{
|
||||||
|
public List<AnchorItemViewModel> 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",
|
||||||
|
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" }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
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<AnchorItemViewModel>? Children { get; set; }
|
||||||
|
}
|
||||||
@@ -86,8 +86,9 @@ public partial class MainViewViewModel : ViewModelBase
|
|||||||
MenuKeys.MenuKeyToolBar => new ToolBarDemoViewModel(),
|
MenuKeys.MenuKeyToolBar => new ToolBarDemoViewModel(),
|
||||||
MenuKeys.MenuKeyTreeComboBox => new TreeComboBoxDemoViewModel(),
|
MenuKeys.MenuKeyTreeComboBox => new TreeComboBoxDemoViewModel(),
|
||||||
MenuKeys.MenuKeyTwoTonePathIcon => new TwoTonePathIconDemoViewModel(),
|
MenuKeys.MenuKeyTwoTonePathIcon => new TwoTonePathIconDemoViewModel(),
|
||||||
MenuKeys.AspectRatioLayout => new AspectRatioLayoutDemoViewModel(),
|
MenuKeys.MenuKeyAspectRatioLayout => new AspectRatioLayoutDemoViewModel(),
|
||||||
MenuKeys.PathPicker => new PathPickerDemoViewModel(),
|
MenuKeys.MenuKeyPathPicker => new PathPickerDemoViewModel(),
|
||||||
|
MenuKeys.MenuKeyAnchor => new AnchorDemoViewModel(),
|
||||||
_ => throw new ArgumentOutOfRangeException(nameof(s), s, null)
|
_ => throw new ArgumentOutOfRangeException(nameof(s), s, null)
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -26,7 +26,7 @@ public class MenuViewModel : ViewModelBase
|
|||||||
new() { MenuHeader = "MultiComboBox", Key = MenuKeys.MenuKeyMultiComboBox },
|
new() { MenuHeader = "MultiComboBox", Key = MenuKeys.MenuKeyMultiComboBox },
|
||||||
new() { MenuHeader = "Numeric UpDown", Key = MenuKeys.MenuKeyNumericUpDown },
|
new() { MenuHeader = "Numeric UpDown", Key = MenuKeys.MenuKeyNumericUpDown },
|
||||||
new() { MenuHeader = "NumPad", Key = MenuKeys.MenuKeyNumPad },
|
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 = "PinCode", Key = MenuKeys.MenuKeyPinCode },
|
||||||
new() { MenuHeader = "RangeSlider", Key = MenuKeys.MenuKeyRangeSlider },
|
new() { MenuHeader = "RangeSlider", Key = MenuKeys.MenuKeyRangeSlider },
|
||||||
new() { MenuHeader = "Rating", Key = MenuKeys.MenuKeyRating },
|
new() { MenuHeader = "Rating", Key = MenuKeys.MenuKeyRating },
|
||||||
@@ -67,6 +67,7 @@ public class MenuViewModel : ViewModelBase
|
|||||||
{
|
{
|
||||||
MenuHeader = "Navigation & Menus", Children = new ObservableCollection<MenuItemViewModel>
|
MenuHeader = "Navigation & Menus", Children = new ObservableCollection<MenuItemViewModel>
|
||||||
{
|
{
|
||||||
|
new() { MenuHeader = "Anchor", Key = MenuKeys.MenuKeyAnchor, Status = "New" },
|
||||||
new() { MenuHeader = "Breadcrumb", Key = MenuKeys.MenuKeyBreadcrumb, Status = "Updated" },
|
new() { MenuHeader = "Breadcrumb", Key = MenuKeys.MenuKeyBreadcrumb, Status = "Updated" },
|
||||||
new() { MenuHeader = "Nav Menu", Key = MenuKeys.MenuKeyNavMenu, Status = "Updated" },
|
new() { MenuHeader = "Nav Menu", Key = MenuKeys.MenuKeyNavMenu, Status = "Updated" },
|
||||||
new() { MenuHeader = "Pagination", Key = MenuKeys.MenuKeyPagination },
|
new() { MenuHeader = "Pagination", Key = MenuKeys.MenuKeyPagination },
|
||||||
@@ -78,7 +79,7 @@ public class MenuViewModel : ViewModelBase
|
|||||||
MenuHeader = "Layout & Display",
|
MenuHeader = "Layout & Display",
|
||||||
Children = new ObservableCollection<MenuItemViewModel>
|
Children = new ObservableCollection<MenuItemViewModel>
|
||||||
{
|
{
|
||||||
new() { MenuHeader = "AspectRatioLayout", Key = MenuKeys.AspectRatioLayout },
|
new() { MenuHeader = "AspectRatioLayout", Key = MenuKeys.MenuKeyAspectRatioLayout },
|
||||||
new() { MenuHeader = "Avatar", Key = MenuKeys.MenuKeyAvatar, Status = "WIP" },
|
new() { MenuHeader = "Avatar", Key = MenuKeys.MenuKeyAvatar, Status = "WIP" },
|
||||||
new() { MenuHeader = "Badge", Key = MenuKeys.MenuKeyBadge },
|
new() { MenuHeader = "Badge", Key = MenuKeys.MenuKeyBadge },
|
||||||
new() { MenuHeader = "Banner", Key = MenuKeys.MenuKeyBanner, Status = "Updated" },
|
new() { MenuHeader = "Banner", Key = MenuKeys.MenuKeyBanner, Status = "Updated" },
|
||||||
@@ -154,6 +155,7 @@ public static class MenuKeys
|
|||||||
public const string MenuKeyToolBar = "ToolBar";
|
public const string MenuKeyToolBar = "ToolBar";
|
||||||
public const string MenuKeyTreeComboBox = "TreeComboBox";
|
public const string MenuKeyTreeComboBox = "TreeComboBox";
|
||||||
public const string MenuKeyTwoTonePathIcon = "TwoTonePathIcon";
|
public const string MenuKeyTwoTonePathIcon = "TwoTonePathIcon";
|
||||||
public const string AspectRatioLayout = "AspectRatioLayout";
|
public const string MenuKeyAspectRatioLayout = "AspectRatioLayout";
|
||||||
public const string PathPicker = "PathPicker";
|
public const string MenuKeyPathPicker = "PathPicker";
|
||||||
|
public const string MenuKeyAnchor = "Anchor";
|
||||||
}
|
}
|
||||||
91
src/Ursa.Themes.Semi/Controls/Anchor.axaml
Normal file
91
src/Ursa.Themes.Semi/Controls/Anchor.axaml
Normal file
@@ -0,0 +1,91 @@
|
|||||||
|
<ResourceDictionary
|
||||||
|
xmlns="https://github.com/avaloniaui"
|
||||||
|
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||||
|
xmlns:converters="clr-namespace:Ursa.Themes.Semi.Converters"
|
||||||
|
xmlns:iri="https://irihi.tech/shared"
|
||||||
|
xmlns:u="https://irihi.tech/ursa">
|
||||||
|
<converters:TreeLevelToPaddingConverter x:Key="LevelToPaddingConverter" />
|
||||||
|
<ControlTheme x:Key="{x:Type u:Anchor}" TargetType="{x:Type u:Anchor}">
|
||||||
|
<Setter Property="Background" Value="Transparent" />
|
||||||
|
<Setter Property="VerticalAlignment" Value="Top" />
|
||||||
|
<Setter Property="Template">
|
||||||
|
<ControlTemplate>
|
||||||
|
<Panel>
|
||||||
|
<Rectangle
|
||||||
|
Width="{DynamicResource AnchorPipeWidth}"
|
||||||
|
Name="PART_Pipe"
|
||||||
|
HorizontalAlignment="Left"
|
||||||
|
VerticalAlignment="Stretch"
|
||||||
|
Fill="{DynamicResource AnchorPipeBackground}" />
|
||||||
|
<ItemsPresenter
|
||||||
|
Name="PART_ItemsPresenter"
|
||||||
|
Margin="{TemplateBinding Padding}"
|
||||||
|
ItemsPanel="{TemplateBinding ItemsPanel}" />
|
||||||
|
</Panel>
|
||||||
|
</ControlTemplate>
|
||||||
|
</Setter>
|
||||||
|
<Style Selector="^.Muted /template/ Rectangle#PART_Pipe">
|
||||||
|
<Setter Property="Fill" Value="Transparent" />
|
||||||
|
</Style>
|
||||||
|
</ControlTheme>
|
||||||
|
|
||||||
|
<ControlTheme x:Key="{x:Type u:AnchorItem}" TargetType="u:AnchorItem">
|
||||||
|
<Setter Property="Background" Value="Transparent" />
|
||||||
|
<Setter Property="Cursor" Value="Hand" />
|
||||||
|
<Setter Property="iri:ClassHelper.ClassSource" Value="{Binding $parent[u:Anchor]}" />
|
||||||
|
<Setter Property="Template">
|
||||||
|
<ControlTemplate TargetType="u:AnchorItem">
|
||||||
|
<StackPanel>
|
||||||
|
<Panel Background="{TemplateBinding Background}">
|
||||||
|
<Border
|
||||||
|
Name="PART_Pipe"
|
||||||
|
Width="{DynamicResource AnchorPipeWidth}"
|
||||||
|
CornerRadius="{DynamicResource AnchorPipeCornerRadius}"
|
||||||
|
HorizontalAlignment="Left"
|
||||||
|
VerticalAlignment="Stretch" />
|
||||||
|
<Panel>
|
||||||
|
<ContentPresenter
|
||||||
|
Name="{x:Static iri:PartNames.PART_HeaderPresenter}"
|
||||||
|
VerticalAlignment="Center"
|
||||||
|
Foreground="{DynamicResource AnchorForeground}"
|
||||||
|
Content="{TemplateBinding Header}"
|
||||||
|
ContentTemplate="{TemplateBinding HeaderTemplate}">
|
||||||
|
<ContentPresenter.Padding>
|
||||||
|
<MultiBinding Converter="{StaticResource LevelToPaddingConverter}">
|
||||||
|
<Binding Path="Level" RelativeSource="{RelativeSource AncestorType={x:Type u:AnchorItem}}" />
|
||||||
|
<DynamicResource ResourceKey="AnchorIndent" />
|
||||||
|
</MultiBinding>
|
||||||
|
</ContentPresenter.Padding>
|
||||||
|
</ContentPresenter>
|
||||||
|
</Panel>
|
||||||
|
</Panel>
|
||||||
|
<ItemsPresenter ItemsPanel="{TemplateBinding ItemsPanel}" />
|
||||||
|
</StackPanel>
|
||||||
|
</ControlTemplate>
|
||||||
|
</Setter>
|
||||||
|
<Style Selector="^ /template/ ContentPresenter#PART_HeaderPresenter">
|
||||||
|
<Setter Property="MinHeight" Value="{DynamicResource AnchorDefaultHeight}" />
|
||||||
|
</Style>
|
||||||
|
<Style Selector="^.Small /template/ ContentPresenter#PART_HeaderPresenter">
|
||||||
|
<Setter Property="MinHeight" Value="{DynamicResource AnchorSmallHeight}" />
|
||||||
|
<Setter Property="FontSize" Value="{DynamicResource AnchorSmallFontSize}" />
|
||||||
|
</Style>
|
||||||
|
<Style Selector="^:selected">
|
||||||
|
<Style Selector="^ /template/ ContentPresenter#PART_HeaderPresenter">
|
||||||
|
<Setter Property="Foreground" Value="{DynamicResource AnchorSelectedForeground}" />
|
||||||
|
</Style>
|
||||||
|
<Style Selector="^ /template/ Border#PART_Pipe">
|
||||||
|
<Setter Property="Background" Value="{DynamicResource AnchorPipeSelectedBackground}" />
|
||||||
|
</Style>
|
||||||
|
<Style Selector="^.Primary /template/ Border#PART_Pipe">
|
||||||
|
<Setter Property="Background" Value="{DynamicResource AnchorPipeSelectedPrimaryBackground}" />
|
||||||
|
</Style>
|
||||||
|
<Style Selector="^.Tertiary /template/ Border#PART_Pipe">
|
||||||
|
<Setter Property="Background" Value="{DynamicResource AnchorPipeSelectedTertiaryBackground}" />
|
||||||
|
</Style>
|
||||||
|
<Style Selector="^.Muted /template/ Border#PART_Pipe">
|
||||||
|
<Setter Property="Background" Value="Transparent" />
|
||||||
|
</Style>
|
||||||
|
</Style>
|
||||||
|
</ControlTheme>
|
||||||
|
</ResourceDictionary>
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
<ResourceDictionary xmlns="https://github.com/avaloniaui" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
|
<ResourceDictionary xmlns="https://github.com/avaloniaui" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
|
||||||
<ResourceDictionary.MergedDictionaries>
|
<ResourceDictionary.MergedDictionaries>
|
||||||
|
<ResourceInclude Source="Anchor.axaml" />
|
||||||
<ResourceInclude Source="AutoCompleteBox.axaml" />
|
<ResourceInclude Source="AutoCompleteBox.axaml" />
|
||||||
<ResourceInclude Source="Avatar.axaml" />
|
<ResourceInclude Source="Avatar.axaml" />
|
||||||
<ResourceInclude Source="Badge.axaml" />
|
<ResourceInclude Source="Badge.axaml" />
|
||||||
|
|||||||
@@ -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<object?> 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();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
using System.Globalization;
|
||||||
|
using Avalonia;
|
||||||
|
using Avalonia.Data.Converters;
|
||||||
|
|
||||||
|
namespace Ursa.Themes.Semi.Converters;
|
||||||
|
|
||||||
|
public class TreeLevelToPaddingConverter : IMultiValueConverter
|
||||||
|
{
|
||||||
|
public object? Convert(IList<object?> values, Type targetType, object? parameter, CultureInfo culture)
|
||||||
|
{
|
||||||
|
if (values[0] is int i && values[1] is Thickness indent)
|
||||||
|
{
|
||||||
|
return new Thickness(Math.Max(i, 0) * indent.Left, indent.Top, indent.Right, indent.Bottom);
|
||||||
|
}
|
||||||
|
|
||||||
|
return new Thickness();
|
||||||
|
}
|
||||||
|
}
|
||||||
8
src/Ursa.Themes.Semi/Themes/Dark/Anchor.axaml
Normal file
8
src/Ursa.Themes.Semi/Themes/Dark/Anchor.axaml
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
<ResourceDictionary xmlns="https://github.com/avaloniaui" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
|
||||||
|
<StaticResource x:Key="AnchorPipeBackground" ResourceKey="SemiColorBorder" />
|
||||||
|
<StaticResource x:Key="AnchorForeground" ResourceKey="SemiColorText2" />
|
||||||
|
<StaticResource x:Key="AnchorSelectedForeground" ResourceKey="SemiColorText0" />
|
||||||
|
<StaticResource x:Key="AnchorPipeSelectedBackground" ResourceKey="SemiColorPrimary" />
|
||||||
|
<StaticResource x:Key="AnchorPipeSelectedPrimaryBackground" ResourceKey="SemiColorPrimary" />
|
||||||
|
<StaticResource x:Key="AnchorPipeSelectedTertiaryBackground" ResourceKey="SemiColorTertiary" />
|
||||||
|
</ResourceDictionary>
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
<ResourceDictionary xmlns="https://github.com/avaloniaui" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
|
<ResourceDictionary xmlns="https://github.com/avaloniaui" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
|
||||||
<ResourceDictionary.MergedDictionaries>
|
<ResourceDictionary.MergedDictionaries>
|
||||||
<ResourceInclude Source="Avatar.axaml" />
|
<ResourceInclude Source="Avatar.axaml" />
|
||||||
|
<ResourceInclude Source="Anchor.axaml" />
|
||||||
<ResourceInclude Source="Badge.axaml" />
|
<ResourceInclude Source="Badge.axaml" />
|
||||||
<ResourceInclude Source="Banner.axaml" />
|
<ResourceInclude Source="Banner.axaml" />
|
||||||
<ResourceInclude Source="Breadcrumb.axaml" />
|
<ResourceInclude Source="Breadcrumb.axaml" />
|
||||||
|
|||||||
7
src/Ursa.Themes.Semi/Themes/HighContrast/Anchor.axaml
Normal file
7
src/Ursa.Themes.Semi/Themes/HighContrast/Anchor.axaml
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
<ResourceDictionary xmlns="https://github.com/avaloniaui" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
|
||||||
|
<StaticResource x:Key="AnchorPipeBackground" ResourceKey="GrayTextColor" />
|
||||||
|
<StaticResource x:Key="AnchorForeground" ResourceKey="WindowTextColor" />
|
||||||
|
<StaticResource x:Key="AnchorSelectedForeground" ResourceKey="HotlightColor" />
|
||||||
|
<StaticResource x:Key="AnchorPipeSelectedBackground" ResourceKey="HotlightColor" />
|
||||||
|
<StaticResource x:Key="AnchorPipeSelectedPrimaryBackground" ResourceKey="HotlightColor" />
|
||||||
|
</ResourceDictionary>
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
<ResourceDictionary xmlns="https://github.com/avaloniaui" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
|
<ResourceDictionary xmlns="https://github.com/avaloniaui" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
|
||||||
<ResourceDictionary.MergedDictionaries>
|
<ResourceDictionary.MergedDictionaries>
|
||||||
|
<ResourceInclude Source="Anchor.axaml" />
|
||||||
<ResourceInclude Source="Badge.axaml" />
|
<ResourceInclude Source="Badge.axaml" />
|
||||||
<ResourceInclude Source="Banner.axaml" />
|
<ResourceInclude Source="Banner.axaml" />
|
||||||
<ResourceInclude Source="Breadcrumb.axaml" />
|
<ResourceInclude Source="Breadcrumb.axaml" />
|
||||||
|
|||||||
8
src/Ursa.Themes.Semi/Themes/Light/Anchor.axaml
Normal file
8
src/Ursa.Themes.Semi/Themes/Light/Anchor.axaml
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
<ResourceDictionary xmlns="https://github.com/avaloniaui" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
|
||||||
|
<StaticResource x:Key="AnchorPipeBackground" ResourceKey="SemiColorBorder" />
|
||||||
|
<StaticResource x:Key="AnchorForeground" ResourceKey="SemiColorText2" />
|
||||||
|
<StaticResource x:Key="AnchorSelectedForeground" ResourceKey="SemiColorText0" />
|
||||||
|
<StaticResource x:Key="AnchorPipeSelectedBackground" ResourceKey="SemiColorPrimary" />
|
||||||
|
<StaticResource x:Key="AnchorPipeSelectedPrimaryBackground" ResourceKey="SemiColorPrimary" />
|
||||||
|
<StaticResource x:Key="AnchorPipeSelectedTertiaryBackground" ResourceKey="SemiColorTertiary" />
|
||||||
|
</ResourceDictionary>
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
<ResourceDictionary xmlns="https://github.com/avaloniaui" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
|
<ResourceDictionary xmlns="https://github.com/avaloniaui" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
|
||||||
<ResourceDictionary.MergedDictionaries>
|
<ResourceDictionary.MergedDictionaries>
|
||||||
<ResourceInclude Source="Avatar.axaml" />
|
<ResourceInclude Source="Avatar.axaml" />
|
||||||
|
<ResourceInclude Source="Anchor.axaml" />
|
||||||
<ResourceInclude Source="Badge.axaml" />
|
<ResourceInclude Source="Badge.axaml" />
|
||||||
<ResourceInclude Source="Banner.axaml" />
|
<ResourceInclude Source="Banner.axaml" />
|
||||||
<ResourceInclude Source="Breadcrumb.axaml" />
|
<ResourceInclude Source="Breadcrumb.axaml" />
|
||||||
|
|||||||
8
src/Ursa.Themes.Semi/Themes/Shared/Anchor.axaml
Normal file
8
src/Ursa.Themes.Semi/Themes/Shared/Anchor.axaml
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
<ResourceDictionary xmlns="https://github.com/avaloniaui" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
|
||||||
|
<Thickness x:Key="AnchorIndent">8,4,0,4</Thickness>
|
||||||
|
<x:Double x:Key="AnchorPipeWidth">2</x:Double>
|
||||||
|
<CornerRadius x:Key="AnchorPipeCornerRadius">1</CornerRadius>
|
||||||
|
<x:Double x:Key="AnchorDefaultHeight">20</x:Double>
|
||||||
|
<x:Double x:Key="AnchorSmallHeight">16</x:Double>
|
||||||
|
<x:Double x:Key="AnchorSmallFontSize">12</x:Double>
|
||||||
|
</ResourceDictionary>
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
<ResourceDictionary xmlns="https://github.com/avaloniaui" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
|
<ResourceDictionary xmlns="https://github.com/avaloniaui" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
|
||||||
<ResourceDictionary.MergedDictionaries>
|
<ResourceDictionary.MergedDictionaries>
|
||||||
<ResourceInclude Source="Avatar.axaml" />
|
<ResourceInclude Source="Avatar.axaml" />
|
||||||
|
<ResourceInclude Source="Anchor.axaml" />
|
||||||
<ResourceInclude Source="Badge.axaml" />
|
<ResourceInclude Source="Badge.axaml" />
|
||||||
<ResourceInclude Source="Banner.axaml" />
|
<ResourceInclude Source="Banner.axaml" />
|
||||||
<ResourceInclude Source="ButtonGroup.axaml" />
|
<ResourceInclude Source="ButtonGroup.axaml" />
|
||||||
|
|||||||
21
src/Ursa/Common/LogicalHelpers.cs
Normal file
21
src/Ursa/Common/LogicalHelpers.cs
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
using Avalonia.LogicalTree;
|
||||||
|
using Ursa.Controls;
|
||||||
|
|
||||||
|
namespace Ursa.Common;
|
||||||
|
|
||||||
|
public static class LogicalHelpers
|
||||||
|
{
|
||||||
|
public static int CalculateDistanceFromLogicalParent<T, TItem>(TItem? item, int defaultValue = -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 : defaultValue;
|
||||||
|
}
|
||||||
|
}
|
||||||
14
src/Ursa/Common/VisualHelpers.cs
Normal file
14
src/Ursa/Common/VisualHelpers.cs
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
using Avalonia;
|
||||||
|
using Avalonia.Controls;
|
||||||
|
using Avalonia.VisualTree;
|
||||||
|
|
||||||
|
namespace Ursa.Common;
|
||||||
|
|
||||||
|
public static class VisualHelpers
|
||||||
|
{
|
||||||
|
public static T? GetContainerFromEventSource<T>(this Visual? source) where T: Control
|
||||||
|
{
|
||||||
|
var item = source?.GetSelfAndVisualAncestors().OfType<T>().FirstOrDefault();
|
||||||
|
return item;
|
||||||
|
}
|
||||||
|
}
|
||||||
221
src/Ursa/Controls/Anchor/Anchor.cs
Normal file
221
src/Ursa/Controls/Anchor/Anchor.cs
Normal file
@@ -0,0 +1,221 @@
|
|||||||
|
using Avalonia;
|
||||||
|
using Avalonia.Animation;
|
||||||
|
using Avalonia.Animation.Easings;
|
||||||
|
using Avalonia.Controls;
|
||||||
|
using Avalonia.Input;
|
||||||
|
using Avalonia.Interactivity;
|
||||||
|
using Avalonia.Styling;
|
||||||
|
using Avalonia.VisualTree;
|
||||||
|
using Ursa.Common;
|
||||||
|
|
||||||
|
namespace Ursa.Controls;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 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.
|
||||||
|
/// </summary>
|
||||||
|
public class Anchor : ItemsControl
|
||||||
|
{
|
||||||
|
public static readonly StyledProperty<ScrollViewer?> TargetContainerProperty =
|
||||||
|
AvaloniaProperty.Register<Anchor, ScrollViewer?>(
|
||||||
|
nameof(TargetContainer));
|
||||||
|
|
||||||
|
public static readonly AttachedProperty<string?> IdProperty =
|
||||||
|
AvaloniaProperty.RegisterAttached<Anchor, Visual, string?>("Id");
|
||||||
|
|
||||||
|
private CancellationTokenSource _cts = new();
|
||||||
|
|
||||||
|
private List<(string, double)> _positions = [];
|
||||||
|
private bool _scrollingFromSelection;
|
||||||
|
|
||||||
|
private AnchorItem? _selectedContainer;
|
||||||
|
|
||||||
|
public ScrollViewer? TargetContainer
|
||||||
|
{
|
||||||
|
get => GetValue(TargetContainerProperty);
|
||||||
|
set => SetValue(TargetContainerProperty, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void SetId(Visual obj, string? value)
|
||||||
|
{
|
||||||
|
obj.SetValue(IdProperty, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static string? GetId(Visual obj)
|
||||||
|
{
|
||||||
|
return obj.GetValue(IdProperty);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static readonly StyledProperty<double> TopOffsetProperty = AvaloniaProperty.Register<Anchor, double>(
|
||||||
|
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<AnchorItem>(item, out recycleKey);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override Control CreateContainerForItemOverride(object? item, int index, object? recycleKey)
|
||||||
|
{
|
||||||
|
var i = new AnchorItem();
|
||||||
|
return i;
|
||||||
|
}
|
||||||
|
|
||||||
|
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 - TopOffset;
|
||||||
|
if (to > TargetContainer.Extent.Height - TargetContainer.Bounds.Height)
|
||||||
|
to = TargetContainer.Extent.Height - TargetContainer.Bounds.Height;
|
||||||
|
if (from == to) return;
|
||||||
|
var 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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
_cts.Cancel();
|
||||||
|
_cts.Dispose();
|
||||||
|
_cts = new CancellationTokenSource();
|
||||||
|
var token = _cts.Token;
|
||||||
|
token.Register(_ => _scrollingFromSelection = false, null);
|
||||||
|
_scrollingFromSelection = true;
|
||||||
|
animation.RunAsync(TargetContainer, token).ContinueWith(_ => _scrollingFromSelection = false, token);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void InvalidatePositions()
|
||||||
|
{
|
||||||
|
InvalidateAnchorPositions();
|
||||||
|
MarkSelectedContainerByPosition();
|
||||||
|
}
|
||||||
|
|
||||||
|
internal void InvalidateAnchorPositions()
|
||||||
|
{
|
||||||
|
if (TargetContainer is null) return;
|
||||||
|
var items = TargetContainer.GetVisualDescendants().Where(a => GetId(a) is not null);
|
||||||
|
var positions = new List<(string, double)>();
|
||||||
|
foreach (var item in items)
|
||||||
|
{
|
||||||
|
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));
|
||||||
|
}
|
||||||
|
|
||||||
|
positions.Sort((a, b) => a.Item2.CompareTo(b.Item2));
|
||||||
|
_positions = positions;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void OnLoaded(RoutedEventArgs e)
|
||||||
|
{
|
||||||
|
base.OnLoaded(e);
|
||||||
|
var target = TargetContainer;
|
||||||
|
if (target is null) return;
|
||||||
|
TargetContainer?.AddHandler(ScrollViewer.ScrollChangedEvent, OnScrollChanged);
|
||||||
|
TargetContainer?.AddHandler(LoadedEvent, OnTargetContainerLoaded);
|
||||||
|
if (TargetContainer?.IsLoaded == true) InvalidateAnchorPositions();
|
||||||
|
MarkSelectedContainerByPosition();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnScrollChanged(object? sender, ScrollChangedEventArgs e)
|
||||||
|
{
|
||||||
|
if (_scrollingFromSelection) return;
|
||||||
|
MarkSelectedContainerByPosition();
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void OnPointerPressed(PointerPressedEventArgs e)
|
||||||
|
{
|
||||||
|
base.OnPointerPressed(e);
|
||||||
|
var source = (e.Source as Visual).GetContainerFromEventSource<AnchorItem>();
|
||||||
|
if (source is null) return;
|
||||||
|
MarkSelectedContainer(source);
|
||||||
|
var target = TargetContainer?.GetVisualDescendants()
|
||||||
|
.FirstOrDefault(a => GetId(a) == source.AnchorId);
|
||||||
|
if (target is null) return;
|
||||||
|
ScrollToAnchor(target);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// This method is used to expose the protected CreateContainerForItemOverride method to the AnchorItem class.
|
||||||
|
/// </summary>
|
||||||
|
internal Control CreateContainerForItemOverrideInternal(object? item, int index, object? recycleKey)
|
||||||
|
{
|
||||||
|
return CreateContainerForItemOverride(item, index, recycleKey);
|
||||||
|
}
|
||||||
|
|
||||||
|
internal bool NeedsContainerOverrideInternal(object? item, int index, out object? recycleKey)
|
||||||
|
{
|
||||||
|
return NeedsContainerOverride(item, index, out recycleKey);
|
||||||
|
}
|
||||||
|
|
||||||
|
internal void PrepareContainerForItemOverrideInternal(Control container, object? item, int index)
|
||||||
|
{
|
||||||
|
PrepareContainerForItemOverride(container, item, index);
|
||||||
|
}
|
||||||
|
|
||||||
|
internal void ContainerForItemPreparedOverrideInternal(Control container, object? item, int index)
|
||||||
|
{
|
||||||
|
ContainerForItemPreparedOverride(container, item, index);
|
||||||
|
}
|
||||||
|
|
||||||
|
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 + TopOffset;
|
||||||
|
var topAnchorId = _positions.LastOrDefault(a => a.Item2 <= top).Item1;
|
||||||
|
if (topAnchorId is null) return;
|
||||||
|
var item = this.GetVisualDescendants().OfType<AnchorItem>()
|
||||||
|
.FirstOrDefault(a => a.AnchorId == topAnchorId);
|
||||||
|
if (item is null) return;
|
||||||
|
MarkSelectedContainer(item);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void OnUnloaded(RoutedEventArgs e)
|
||||||
|
{
|
||||||
|
base.OnUnloaded(e);
|
||||||
|
TargetContainer?.RemoveHandler(LoadedEvent, OnTargetContainerLoaded);
|
||||||
|
TargetContainer?.RemoveHandler(ScrollViewer.ScrollChangedEvent, OnScrollChanged);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnTargetContainerLoaded(object? sender, RoutedEventArgs e)
|
||||||
|
{
|
||||||
|
InvalidateAnchorPositions();
|
||||||
|
}
|
||||||
|
}
|
||||||
91
src/Ursa/Controls/Anchor/AnchorItem.cs
Normal file
91
src/Ursa/Controls/Anchor/AnchorItem.cs
Normal file
@@ -0,0 +1,91 @@
|
|||||||
|
using Avalonia;
|
||||||
|
using Avalonia.Controls;
|
||||||
|
using Avalonia.Controls.Mixins;
|
||||||
|
using Avalonia.Controls.Primitives;
|
||||||
|
using Avalonia.Controls.Templates;
|
||||||
|
using Avalonia.LogicalTree;
|
||||||
|
using Ursa.Common;
|
||||||
|
|
||||||
|
namespace Ursa.Controls;
|
||||||
|
|
||||||
|
public class AnchorItem : HeaderedItemsControl, ISelectable
|
||||||
|
{
|
||||||
|
public static readonly StyledProperty<string?> AnchorIdProperty = AvaloniaProperty.Register<AnchorItem, string?>(
|
||||||
|
nameof(AnchorId));
|
||||||
|
|
||||||
|
public static readonly StyledProperty<bool> IsSelectedProperty =
|
||||||
|
SelectingItemsControl.IsSelectedProperty.AddOwner<AnchorItem>();
|
||||||
|
|
||||||
|
private static readonly FuncTemplate<Panel?> DefaultPanel =
|
||||||
|
new(() => new StackPanel());
|
||||||
|
|
||||||
|
internal static readonly DirectProperty<AnchorItem, int> LevelProperty =
|
||||||
|
AvaloniaProperty.RegisterDirect<AnchorItem, int>(
|
||||||
|
nameof(Level), o => o.Level, (o, v) => o.Level = v);
|
||||||
|
|
||||||
|
private int _level;
|
||||||
|
|
||||||
|
private Anchor? _root;
|
||||||
|
|
||||||
|
static AnchorItem()
|
||||||
|
{
|
||||||
|
SelectableMixin.Attach<AnchorItem>(IsSelectedProperty);
|
||||||
|
PressedMixin.Attach<AnchorItem>();
|
||||||
|
ItemsPanelProperty.OverrideDefaultValue<AnchorItem>(DefaultPanel);
|
||||||
|
}
|
||||||
|
|
||||||
|
public int Level
|
||||||
|
{
|
||||||
|
get => _level;
|
||||||
|
set => SetAndRaise(LevelProperty, ref _level, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
public string? AnchorId
|
||||||
|
{
|
||||||
|
get => GetValue(AnchorIdProperty);
|
||||||
|
set => SetValue(AnchorIdProperty, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool IsSelected
|
||||||
|
{
|
||||||
|
get => GetValue(IsSelectedProperty);
|
||||||
|
set => SetValue(IsSelectedProperty, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void OnAttachedToVisualTree(VisualTreeAttachmentEventArgs e)
|
||||||
|
{
|
||||||
|
base.OnAttachedToVisualTree(e);
|
||||||
|
_root = this.GetLogicalAncestors().OfType<Anchor>().FirstOrDefault();
|
||||||
|
Level = LogicalHelpers.CalculateDistanceFromLogicalParent<Anchor, AnchorItem>(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().CreateContainerForItemOverrideInternal(item, index, recycleKey);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override bool NeedsContainerOverride(object? item, int index, out object? recycleKey)
|
||||||
|
{
|
||||||
|
return EnsureRoot().NeedsContainerOverrideInternal(item, index, out recycleKey);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void PrepareContainerForItemOverride(Control container, object? item, int index)
|
||||||
|
{
|
||||||
|
EnsureRoot().PrepareContainerForItemOverrideInternal(container, item, index);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void ContainerForItemPreparedOverride(Control container, object? item, int index)
|
||||||
|
{
|
||||||
|
EnsureRoot().ContainerForItemPreparedOverrideInternal(container, item, index);
|
||||||
|
}
|
||||||
|
|
||||||
|
private Anchor EnsureRoot()
|
||||||
|
{
|
||||||
|
return _root ?? throw new InvalidOperationException("AnchorItem must be inside an Anchor control.");
|
||||||
|
}
|
||||||
|
}
|
||||||
90
tests/HeadlessTest.Ursa/Controls/AnchorTests/TestView.axaml
Normal file
90
tests/HeadlessTest.Ursa/Controls/AnchorTests/TestView.axaml
Normal file
@@ -0,0 +1,90 @@
|
|||||||
|
<UserControl
|
||||||
|
x:Class="HeadlessTest.Ursa.Controls.AnchorTests.TestView"
|
||||||
|
xmlns="https://github.com/avaloniaui"
|
||||||
|
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||||
|
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||||
|
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||||
|
xmlns:u="https://irihi.tech/ursa"
|
||||||
|
d:DesignHeight="450"
|
||||||
|
d:DesignWidth="800"
|
||||||
|
mc:Ignorable="d">
|
||||||
|
<Grid ColumnDefinitions="Auto, *">
|
||||||
|
<ScrollViewer
|
||||||
|
Name="ScrollViewer"
|
||||||
|
Grid.Column="1"
|
||||||
|
VerticalAlignment="Stretch">
|
||||||
|
<StackPanel>
|
||||||
|
<StackPanel.Styles>
|
||||||
|
<Style Selector="TextBlock">
|
||||||
|
<Setter Property="HorizontalAlignment" Value="Center" />
|
||||||
|
<Setter Property="VerticalAlignment" Value="Center" />
|
||||||
|
</Style>
|
||||||
|
</StackPanel.Styles>
|
||||||
|
<Border
|
||||||
|
Height="300"
|
||||||
|
HorizontalAlignment="Stretch"
|
||||||
|
u:Anchor.Id="a1"
|
||||||
|
Background="{DynamicResource SemiRed2}">
|
||||||
|
<TextBlock Text="Border 1" />
|
||||||
|
</Border>
|
||||||
|
<Border
|
||||||
|
Height="300"
|
||||||
|
HorizontalAlignment="Stretch"
|
||||||
|
u:Anchor.Id="a2"
|
||||||
|
Background="{DynamicResource SemiPink1}">
|
||||||
|
<TextBlock Text="Border 2" />
|
||||||
|
</Border>
|
||||||
|
<Border
|
||||||
|
Height="300"
|
||||||
|
HorizontalAlignment="Stretch"
|
||||||
|
u:Anchor.Id="a3"
|
||||||
|
Background="{DynamicResource SemiPurple1}">
|
||||||
|
<TextBlock Text="Border 3" />
|
||||||
|
</Border>
|
||||||
|
<Border
|
||||||
|
Height="300"
|
||||||
|
u:Anchor.Id="a4"
|
||||||
|
HorizontalAlignment="Stretch"
|
||||||
|
Background="{DynamicResource SemiViolet1}">
|
||||||
|
<TextBlock Text="Border 4" />
|
||||||
|
</Border>
|
||||||
|
<Border
|
||||||
|
Height="300"
|
||||||
|
HorizontalAlignment="Stretch"
|
||||||
|
u:Anchor.Id="a5"
|
||||||
|
Background="{DynamicResource SemiIndigo1}">
|
||||||
|
<TextBlock Text="Border 5" />
|
||||||
|
</Border>
|
||||||
|
<Border
|
||||||
|
Height="300"
|
||||||
|
HorizontalAlignment="Stretch"
|
||||||
|
u:Anchor.Id="a6"
|
||||||
|
Background="{DynamicResource SemiBlue1}">
|
||||||
|
<TextBlock Text="Border 6" />
|
||||||
|
</Border>
|
||||||
|
<Border
|
||||||
|
Height="300"
|
||||||
|
HorizontalAlignment="Stretch"
|
||||||
|
u:Anchor.Id="a7"
|
||||||
|
Background="{DynamicResource SemiLightBlue1}">
|
||||||
|
<TextBlock Text="Border 7" />
|
||||||
|
</Border>
|
||||||
|
</StackPanel>
|
||||||
|
</ScrollViewer>
|
||||||
|
<u:Anchor
|
||||||
|
Name="Anchor"
|
||||||
|
Grid.Column="0"
|
||||||
|
Width="200"
|
||||||
|
TargetContainer="{Binding ElementName=ScrollViewer}">
|
||||||
|
<u:AnchorItem Name="Item1" AnchorId="a1" Header="Rectangle 1">
|
||||||
|
<u:AnchorItem Name="Item2" AnchorId="a2" Header="Rectangle 2" />
|
||||||
|
<u:AnchorItem AnchorId="a3" Header="Rectangle 3" />
|
||||||
|
</u:AnchorItem>
|
||||||
|
<u:AnchorItem Name="Item4" AnchorId="a4" Header="Rectangle 4" />
|
||||||
|
<u:AnchorItem AnchorId="a5" Header="Rectangle 5">
|
||||||
|
<u:AnchorItem AnchorId="a6" Header="Rectangle 6" />
|
||||||
|
<u:AnchorItem AnchorId="a7" Header="Rectangle 7" />
|
||||||
|
</u:AnchorItem>
|
||||||
|
</u:Anchor>
|
||||||
|
</Grid>
|
||||||
|
</UserControl>
|
||||||
@@ -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();
|
||||||
|
}
|
||||||
|
}
|
||||||
27
tests/HeadlessTest.Ursa/Controls/AnchorTests/TestView2.axaml
Normal file
27
tests/HeadlessTest.Ursa/Controls/AnchorTests/TestView2.axaml
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
<UserControl xmlns="https://github.com/avaloniaui"
|
||||||
|
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||||
|
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:vm="clr-namespace:HeadlessTest.Ursa.Controls.AnchorTests"
|
||||||
|
mc:Ignorable="d" d:DesignWidth="800"
|
||||||
|
d:DesignHeight="450"
|
||||||
|
x:DataType="vm:AnchorDemoViewModel"
|
||||||
|
x:CompileBindings="True"
|
||||||
|
x:Class="HeadlessTest.Ursa.Controls.AnchorTests.TestView2">
|
||||||
|
<u:Anchor
|
||||||
|
Width="200"
|
||||||
|
Margin="24"
|
||||||
|
ItemsSource="{Binding AnchorItems}">
|
||||||
|
<u:Anchor.Styles>
|
||||||
|
<Style x:DataType="vm:AnchorItemViewModel" Selector="u|AnchorItem">
|
||||||
|
<Setter Property="AnchorId" Value="{Binding AnchorId}" />
|
||||||
|
</Style>
|
||||||
|
</u:Anchor.Styles>
|
||||||
|
<u:Anchor.ItemTemplate>
|
||||||
|
<TreeDataTemplate ItemsSource="{Binding Children}">
|
||||||
|
<TextBlock Text="{Binding Header}"/>
|
||||||
|
</TreeDataTemplate>
|
||||||
|
</u:Anchor.ItemTemplate>
|
||||||
|
</u:Anchor>
|
||||||
|
</UserControl>
|
||||||
@@ -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<AnchorItemViewModel> 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<AnchorItemViewModel>? Children { get; set; }
|
||||||
|
}
|
||||||
97
tests/HeadlessTest.Ursa/Controls/AnchorTests/Tests.cs
Normal file
97
tests/HeadlessTest.Ursa/Controls/AnchorTests/Tests.cs
Normal file
@@ -0,0 +1,97 @@
|
|||||||
|
using Avalonia;
|
||||||
|
using Avalonia.Controls;
|
||||||
|
using Avalonia.Headless;
|
||||||
|
using Avalonia.Headless.XUnit;
|
||||||
|
using Avalonia.Input;
|
||||||
|
using Avalonia.Threading;
|
||||||
|
using Avalonia.VisualTree;
|
||||||
|
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>("Anchor");
|
||||||
|
var scrollViewer = view.FindControl<ScrollViewer>("ScrollViewer");
|
||||||
|
var item4 = view.FindControl<AnchorItem>("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, 50.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>("Anchor");
|
||||||
|
var scrollViewer = view.FindControl<ScrollViewer>("ScrollViewer");
|
||||||
|
var item1 = view.FindControl<AnchorItem>("Item1");
|
||||||
|
var item2 = view.FindControl<AnchorItem>("Item2");
|
||||||
|
var item4 = view.FindControl<AnchorItem>("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);
|
||||||
|
}
|
||||||
|
|
||||||
|
[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<AnchorItem>().ToList();
|
||||||
|
Assert.NotEmpty(items);
|
||||||
|
Assert.Equal(10, items.Count);
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user