完成基础的 Collapse 功能

This commit is contained in:
polarboy 2024-08-08 20:54:39 +08:00
parent eb6ad5549b
commit 86f692cdab
18 changed files with 872 additions and 53 deletions

View File

@ -1,37 +1,31 @@
<Project>
<PropertyGroup>
<ManagePackageVersionsCentrally>true</ManagePackageVersionsCentrally>
</PropertyGroup>
<ItemGroup>
<PackageVersion Include="Avalonia" Version="$(AvaloniaVersionThatLibrariesUsed)"/>
<PackageVersion Include="Avalonia.Desktop" Version="$(AvaloniaVersionThatLibrariesUsed)"/>
<PackageVersion Include="Avalonia.Diagnostics" Version="$(AvaloniaVersionThatLibrariesUsed)"/>
<PackageVersion Include="Avalonia.Controls.ColorPicker" Version="$(AvaloniaVersionThatLibrariesUsed)"/>
<PackageVersion Include="Avalonia.Controls.DataGrid" Version="$(AvaloniaVersionThatLibrariesUsed)"/>
<PackageVersion Include="Avalonia.Svg" Version="$(AvaloniaVersionThatLibrariesUsed)"/>
<PackageVersion Include="Avalonia.Controls.TreeDataGrid" Version="11.0.10"/>
<PackageVersion Include="Avalonia.Themes.Fluent" Version="$(AvaloniaVersionThatLibrariesUsed)"/>
<PackageVersion Include="Avalonia.Win32" Version="$(AvaloniaVersionThatLibrariesUsed)"/>
<PackageVersion Include="System.Reactive" Version="6.0.1" />
<!-- 测试相关依赖 -->
<PackageVersion Include="Microsoft.NET.Test.Sdk" Version="17.10.0-release-24177-07" />
<PackageVersion Include="NSubstitute" Version="5.1.0" />
<PackageVersion Include="Shouldly" Version="4.2.1" />
<PackageVersion Include="xunit" Version="2.8.0" />
<PackageVersion Include="xunit.extensibility.execution" Version="2.8.0" />
<PackageVersion Include="xunit.runner.visualstudio" Version="2.8.0" />
<PackageVersion Include="CommunityToolkit.Mvvm" Version="8.1.0" />
<!-- 开发支持 -->
<PackageVersion Include="Nlnet.Avalonia.DevTools" Version="1.0.1-beta.23" />
<!-- 源码生成 -->
<PackageVersion Include="Microsoft.CodeAnalysis.CSharp" Version="4.10.0-3.final" />
<PackageVersion Include="Microsoft.CodeAnalysis.Analyzers" Version="3.11.0-beta1.24219.2" />
</ItemGroup>
<Import Project="$(MSBuildThisFileDirectory)\Build\Common.props" />
<PropertyGroup>
<ManagePackageVersionsCentrally>true</ManagePackageVersionsCentrally>
</PropertyGroup>
<ItemGroup>
<PackageVersion Include="Avalonia" Version="$(AvaloniaVersionThatLibrariesUsed)" />
<PackageVersion Include="Avalonia.Desktop" Version="$(AvaloniaVersionThatLibrariesUsed)" />
<PackageVersion Include="Avalonia.Diagnostics" Version="$(AvaloniaVersionThatLibrariesUsed)" />
<PackageVersion Include="Avalonia.Controls.ColorPicker" Version="$(AvaloniaVersionThatLibrariesUsed)" />
<PackageVersion Include="Avalonia.Controls.DataGrid" Version="$(AvaloniaVersionThatLibrariesUsed)" />
<PackageVersion Include="Avalonia.Svg" Version="11.1.0" />
<PackageVersion Include="Avalonia.Controls.TreeDataGrid" Version="11.0.10" />
<PackageVersion Include="Avalonia.Themes.Fluent" Version="$(AvaloniaVersionThatLibrariesUsed)" />
<PackageVersion Include="Avalonia.Win32" Version="$(AvaloniaVersionThatLibrariesUsed)" />
<PackageVersion Include="System.Reactive" Version="6.0.1" />
<!-- 测试相关依赖 -->
<PackageVersion Include="Microsoft.NET.Test.Sdk" Version="17.10.0-release-24177-07" />
<PackageVersion Include="NSubstitute" Version="5.1.0" />
<PackageVersion Include="Shouldly" Version="4.2.1" />
<PackageVersion Include="xunit" Version="2.8.0" />
<PackageVersion Include="xunit.extensibility.execution" Version="2.8.0" />
<PackageVersion Include="xunit.runner.visualstudio" Version="2.8.0" />
<PackageVersion Include="CommunityToolkit.Mvvm" Version="8.1.0" />
<!-- 开发支持 -->
<PackageVersion Include="Nlnet.Avalonia.DevTools" Version="1.0.1-beta.23" />
<!-- 源码生成 -->
<PackageVersion Include="Microsoft.CodeAnalysis.CSharp" Version="4.10.0-3.final" />
<PackageVersion Include="Microsoft.CodeAnalysis.Analyzers" Version="3.11.0-beta1.24219.2" />
</ItemGroup>
</Project>

View File

@ -1,9 +1,8 @@
<p align="center">
<img src="./docs/images/ATOMUI.png" />
</p>
<p align="center">
<h4>基于 Ant Design 5.0 和 Avalonia 技术的企业级跨平台控件库</h4>
</p>
<div align="center" style = "font-weight: bold">基于 Ant Design 5.0 和 Avalonia 技术的企业级跨平台控件库</div>
#### 介绍

View File

@ -3,8 +3,8 @@
<NoWarn>$(NoWarn);CS7035</NoWarn>
<AvaloniaVersionThatLibrariesUsed>11.1.0</AvaloniaVersionThatLibrariesUsed>
<AvaloniaVersionThatSampleUsed>11.1.0</AvaloniaVersionThatSampleUsed>
<AvaloniaVersionThatLibrariesUsed>11.1.2</AvaloniaVersionThatLibrariesUsed>
<AvaloniaVersionThatSampleUsed>11.1.2</AvaloniaVersionThatSampleUsed>
<LibVersion>0.0.1</LibVersion>
<NugetPackageVersion>$(LibVersion)-local.1</NugetPackageVersion>
<NugetPackageVersion>$(LibVersion)-preview.1</NugetPackageVersion>

View File

@ -1,6 +1,5 @@
<Project Sdk="Microsoft.NET.Sdk">
<Import Project="$(MSBuildThisFileDirectory)..\..\Build\Common.props" />
<Import Project="$(MSBuildThisFileDirectory)..\..\Build\Output.App.props" />
<PropertyGroup>

View File

@ -0,0 +1,33 @@
<UserControl
x:Class="AtomUI.Demo.Desktop.ShowCase.CollapseShowCase"
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:atom="https://atomui.net"
xmlns:showcase="clr-namespace:AtomUI.Demo.Desktop.ShowCase"
mc:Ignorable="d">
<showcase:ShowCasePanel>
<showcase:ShowCaseItem
Title="Collapse"
Description="By default, any number of panels can be expanded at a time. The first panel is expanded in this example.">
<atom:Collapse>
<atom:CollapseItem Header="This is panel header 1">
<TextBlock TextWrapping="Wrap">
A dog is a type of domesticated animal. Known for its loyalty and faithfulness, it can be found as a welcome guest in many households across the world.
</TextBlock>
</atom:CollapseItem>
<atom:CollapseItem Header="This is panel header 2">
<TextBlock TextWrapping="Wrap">
A dog is a type of domesticated animal. Known for its loyalty and faithfulness, it can be found as a welcome guest in many households across the world.
</TextBlock>
</atom:CollapseItem>
<atom:CollapseItem Header="This is panel header 3">
<TextBlock TextWrapping="Wrap">
A dog is a type of domesticated animal. Known for its loyalty and faithfulness, it can be found as a welcome guest in many households across the world.
</TextBlock>
</atom:CollapseItem>
</atom:Collapse>
</showcase:ShowCaseItem>
</showcase:ShowCasePanel>
</UserControl>

View File

@ -0,0 +1,11 @@
using Avalonia.Controls;
namespace AtomUI.Demo.Desktop.ShowCase;
public partial class CollapseShowCase : UserControl
{
public CollapseShowCase()
{
InitializeComponent();
}
}

View File

@ -93,6 +93,9 @@
<TabItem Header="Expander">
<showcases:ExpanderShowCase />
</TabItem>
<TabItem Header="Collapse">
<showcases:CollapseShowCase />
</TabItem>
<TabItem Header="ComboBox">
<showcases:ComboBoxShowCase />
</TabItem>

View File

@ -1,7 +1,5 @@
<Project Sdk="Microsoft.NET.Sdk">
<Import Project="$(MSBuildThisFileDirectory)..\..\Build\Common.props" />
<PropertyGroup>
<AllowUnsafeBlocks>True</AllowUnsafeBlocks>
</PropertyGroup>

View File

@ -1,6 +1,8 @@
using AtomUI.Icon;
using AtomUI.Theme.Styling;
using Avalonia;
using Avalonia.Controls;
using Avalonia.Controls.Primitives;
using Avalonia.Input;
using Avalonia.Layout;
using Avalonia.LogicalTree;
@ -33,22 +35,26 @@ public class IconButton : AvaloniaButton, ICustomHitTest
Cursor = new Cursor(StandardCursorType.Hand);
}
protected override void OnAttachedToLogicalTree(LogicalTreeAttachmentEventArgs e)
protected override void OnApplyTemplate(TemplateAppliedEventArgs e)
{
base.OnAttachedToLogicalTree(e);
base.OnApplyTemplate(e);
if (Icon is not null) {
Icon.SetCurrentValue(PathIcon.HorizontalAlignmentProperty, HorizontalAlignment.Center);
Icon.SetCurrentValue(PathIcon.VerticalAlignmentProperty, VerticalAlignment.Center);
Content = Icon;
}
}
protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs e)
{
base.OnPropertyChanged(e);
if (VisualRoot is not null) {
if (e.Property == IconProperty) {
var oldIcon = e.GetOldValue<PathIcon?>();
if (oldIcon is not null) {
((ISetLogicalParent)oldIcon).SetParent(null);
}
Content = e.GetNewValue<PathIcon?>();
} else if (e.Property == IsPressedProperty ||
e.Property == IsPointerOverProperty) {

View File

@ -0,0 +1,256 @@
using AtomUI.Controls.Utils;
using AtomUI.Data;
using AtomUI.Theme.Data;
using AtomUI.Theme.Styling;
using AtomUI.Utils;
using Avalonia;
using Avalonia.Controls;
using Avalonia.Controls.Metadata;
using Avalonia.Controls.Presenters;
using Avalonia.Controls.Primitives;
using Avalonia.Controls.Templates;
using Avalonia.Data;
using Avalonia.Input;
using Avalonia.Layout;
using Avalonia.VisualTree;
namespace AtomUI.Controls;
public enum CollapseTriggerType
{
Header,
Icon,
}
public enum CollapseExpandIconPosition
{
Start,
End,
}
[TemplatePart(CollapseTheme.ItemsPresenterPart, typeof(ItemsPresenter))]
public class Collapse : SelectingItemsControl
{
#region
public static readonly StyledProperty<SizeType> SizeTypeProperty =
AvaloniaProperty.Register<Collapse, SizeType>(nameof(SizeType), SizeType.Middle);
public static readonly StyledProperty<bool> IsGhostStyleProperty =
AvaloniaProperty.Register<Collapse, bool>(nameof(IsGhostStyle), false);
public static readonly StyledProperty<bool> IsBorderlessProperty =
AvaloniaProperty.Register<Collapse, bool>(nameof(IsBorderless), false);
public static readonly StyledProperty<bool> IsAccordionProperty =
AvaloniaProperty.Register<Collapse, bool>(nameof(IsAccordion), false);
public static readonly StyledProperty<CollapseTriggerType> TriggerTypeProperty =
AvaloniaProperty.Register<Collapse, CollapseTriggerType>(nameof(TriggerType), CollapseTriggerType.Header);
public static readonly StyledProperty<CollapseExpandIconPosition> ExpandIconPositionProperty =
AvaloniaProperty.Register<Collapse, CollapseExpandIconPosition>(nameof(ExpandIconPosition), CollapseExpandIconPosition.Start);
public SizeType SizeType
{
get => GetValue(SizeTypeProperty);
set => SetValue(SizeTypeProperty, value);
}
public bool IsGhostStyle
{
get => GetValue(IsGhostStyleProperty);
set => SetValue(IsGhostStyleProperty, value);
}
public bool IsBorderless
{
get => GetValue(IsBorderlessProperty);
set => SetValue(IsBorderlessProperty, value);
}
public bool IsAccordion
{
get => GetValue(IsAccordionProperty);
set => SetValue(IsAccordionProperty, value);
}
public CollapseTriggerType TriggerType
{
get => GetValue(TriggerTypeProperty);
set => SetValue(TriggerTypeProperty, value);
}
public CollapseExpandIconPosition ExpandIconPosition
{
get => GetValue(ExpandIconPositionProperty);
set => SetValue(ExpandIconPositionProperty, value);
}
#endregion
#region
internal static readonly DirectProperty<Collapse, Thickness> EffectiveBorderThicknessProperty =
AvaloniaProperty.RegisterDirect<Collapse, Thickness>(nameof(EffectiveBorderThickness),
o => o.EffectiveBorderThickness,
(o, v) => o.EffectiveBorderThickness = v);
private Thickness _effectiveBorderThickness;
internal Thickness EffectiveBorderThickness
{
get => _effectiveBorderThickness;
set => SetAndRaise(EffectiveBorderThicknessProperty, ref _effectiveBorderThickness, value);
}
internal ItemsPresenter? ItemsPresenterPart { get; private set; }
private static readonly FuncTemplate<Panel?> DefaultPanel =
new(() => new StackPanel
{
Orientation = Orientation.Vertical
});
#endregion
static Collapse()
{
SelectionModeProperty.OverrideDefaultValue<Collapse>(SelectionMode.Multiple | SelectionMode.Toggle);
ItemsPanelProperty.OverrideDefaultValue<Collapse>(DefaultPanel);
}
public Collapse()
{
SelectionChanged += HandleSelectionChanged;
}
private void HandleSelectionChanged(object? sender, SelectionChangedEventArgs args)
{
if (VisualRoot is not null) {
for (var i = 0; i < ItemCount; ++i) {
if (Items[i] is CollapseItem collapseItem) {
SetupCollapseBorderThickness(collapseItem, i);
}
}
}
}
protected override void OnApplyTemplate(TemplateAppliedEventArgs e)
{
base.OnApplyTemplate(e);
TokenResourceBinder.CreateGlobalResourceBinding(this, BorderThicknessProperty, GlobalResourceKey.BorderThickness,
BindingPriority.Template, new RenderScaleAwareThicknessConfigure(this));
SetupEffectiveBorderThickness();
SetupSelectionMode();
}
protected override Control CreateContainerForItemOverride(object? item, int index, object? recycleKey)
{
return new CollapseItem();
}
protected override bool NeedsContainerOverride(object? item, int index, out object? recycleKey)
{
return NeedsContainer<CollapseItem>(item, out recycleKey);
}
protected override void PrepareContainerForItemOverride(Control element, object? item, int index)
{
base.PrepareContainerForItemOverride(element, item, index);
if (item is CollapseItem collapseItem) {
BindUtils.RelayBind(this, SizeTypeProperty, collapseItem, CollapseItem.SizeTypeProperty);
BindUtils.RelayBind(this, EffectiveBorderThicknessProperty, collapseItem, CollapseItem.BorderThicknessProperty);
BindUtils.RelayBind(this, IsGhostStyleProperty, collapseItem, CollapseItem.IsGhostStyleProperty);
BindUtils.RelayBind(this, TriggerTypeProperty, collapseItem, CollapseItem.TriggerTypeProperty);
BindUtils.RelayBind(this, ExpandIconPositionProperty, collapseItem, CollapseItem.ExpandIconPositionProperty);
SetupCollapseBorderThickness(collapseItem, index);
}
}
private void SetupCollapseBorderThickness(CollapseItem collapseItem, int index)
{
var headerBorderBottom = BorderThickness.Bottom;
if (index == ItemCount - 1 && !collapseItem.IsSelected) {
headerBorderBottom = 0d;
}
collapseItem.HeaderBorderThickness = new Thickness(0, 0, 0, headerBorderBottom);
var contentBorderBottom = BorderThickness.Bottom;
if (index == ItemCount - 1 && collapseItem.IsSelected) {
contentBorderBottom = 0d;
}
collapseItem.ContentBorderThickness = new Thickness(0, 0, 0, contentBorderBottom);
}
protected override void ContainerIndexChangedOverride(Control container, int oldIndex, int newIndex)
{
base.ContainerIndexChangedOverride(container, oldIndex, newIndex);
}
protected override void ClearContainerForItemOverride(Control element)
{
base.ClearContainerForItemOverride(element);
}
protected override void OnGotFocus(GotFocusEventArgs e)
{
base.OnGotFocus(e);
if (e.NavigationMethod == NavigationMethod.Directional) {
e.Handled = UpdateSelectionFromEventSource(e.Source);
}
}
protected override void OnPointerPressed(PointerPressedEventArgs e)
{
base.OnPointerPressed(e);
if (e.GetCurrentPoint(this).Properties.IsLeftButtonPressed && e.Pointer.Type == PointerType.Mouse) {
e.Handled = UpdateSelectionFromEventSource(e.Source);
}
}
protected override void OnPointerReleased(PointerReleasedEventArgs e)
{
if (e.InitialPressMouseButton == MouseButton.Left && e.Pointer.Type != PointerType.Mouse) {
var container = GetContainerFromEventSource(e.Source);
if (container != null
&& container.GetVisualsAt(e.GetPosition(container))
.Any(c => container == c || container.IsVisualAncestorOf(c))) {
e.Handled = UpdateSelectionFromEventSource(e.Source);
}
}
}
protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change)
{
base.OnPropertyChanged(change);
if (VisualRoot is not null) {
if (change.Property == IsBorderlessProperty) {
SetupEffectiveBorderThickness();
}
}
if (change.Property == IsAccordionProperty) {
SetupSelectionMode();
}
}
private void SetupEffectiveBorderThickness()
{
if (IsBorderless) {
EffectiveBorderThickness = default;
} else {
EffectiveBorderThickness = BorderThickness;
}
}
private void SetupSelectionMode()
{
if (IsAccordion) {
SelectionMode = SelectionMode.Single | SelectionMode.Toggle;
} else {
SelectionMode = SelectionMode.Multiple | SelectionMode.Toggle;
}
}
}

View File

@ -0,0 +1,195 @@
using AtomUI.Controls.Utils;
using Avalonia;
using Avalonia.Automation;
using Avalonia.Automation.Peers;
using Avalonia.Controls;
using Avalonia.Controls.Metadata;
using Avalonia.Controls.Mixins;
using Avalonia.Controls.Primitives;
namespace AtomUI.Controls;
[PseudoClasses(StdPseudoClass.Pressed, StdPseudoClass.Selected)]
public class CollapseItem : HeaderedContentControl, ISelectable
{
#region
public static readonly StyledProperty<bool> IsSelectedProperty =
SelectingItemsControl.IsSelectedProperty.AddOwner<CollapseItem>();
public static readonly StyledProperty<bool> IsShowExpandIconProperty =
AvaloniaProperty.Register<CollapseItem, bool>(nameof(IsShowExpandIcon));
public static readonly StyledProperty<PathIcon?> ExpandIconProperty =
AvaloniaProperty.Register<CollapseItem, PathIcon?>(nameof(ExpandIcon));
public static readonly StyledProperty<object?> AddOnContentProperty =
AvaloniaProperty.Register<CollapseItem, object?>(nameof(AddOnContent));
public bool IsSelected
{
get => GetValue(IsSelectedProperty);
set => SetValue(IsSelectedProperty, value);
}
public bool IsShowExpandIcon
{
get => GetValue(IsShowExpandIconProperty);
set => SetValue(IsShowExpandIconProperty, value);
}
public PathIcon? ExpandIcon
{
get => GetValue(ExpandIconProperty);
set => SetValue(ExpandIconProperty, value);
}
public object? AddOnContent
{
get => GetValue(AddOnContentProperty);
set => SetValue(AddOnContentProperty, value);
}
#endregion
#region
internal static readonly DirectProperty<CollapseItem, SizeType> SizeTypeProperty =
AvaloniaProperty.RegisterDirect<CollapseItem, SizeType>(nameof(SizeType),
o => o.SizeType,
(o, v) => o.SizeType = v);
internal static readonly DirectProperty<CollapseItem, bool> IsGhostStyleProperty =
AvaloniaProperty.RegisterDirect<CollapseItem, bool>(nameof(IsGhostStyle),
o => o.IsGhostStyle,
(o, v) => o.IsGhostStyle = v);
internal static readonly DirectProperty<CollapseItem, CollapseTriggerType> TriggerTypeProperty =
AvaloniaProperty.RegisterDirect<CollapseItem, CollapseTriggerType>(nameof(TriggerType),
o => o.TriggerType,
(o, v) => o.TriggerType = v);
internal static readonly DirectProperty<CollapseItem, CollapseExpandIconPosition> ExpandIconPositionProperty =
AvaloniaProperty.RegisterDirect<CollapseItem, CollapseExpandIconPosition>(nameof(ExpandIconPosition),
o => o.ExpandIconPosition,
(o, v) => o.ExpandIconPosition = v);
internal static readonly DirectProperty<CollapseItem, Thickness> HeaderBorderThicknessProperty =
AvaloniaProperty.RegisterDirect<CollapseItem, Thickness>(nameof(HeaderBorderThickness),
o => o.HeaderBorderThickness,
(o, v) => o.HeaderBorderThickness = v);
internal static readonly DirectProperty<CollapseItem, Thickness> ContentBorderThicknessProperty =
AvaloniaProperty.RegisterDirect<CollapseItem, Thickness>(nameof(ContentBorderThickness),
o => o. ContentBorderThickness,
(o, v) => o. ContentBorderThickness = v);
private SizeType _sizeType;
internal SizeType SizeType
{
get => _sizeType;
set => SetAndRaise(SizeTypeProperty, ref _sizeType, value);
}
private bool _isGhostStyle = false;
internal bool IsGhostStyle
{
get => _isGhostStyle;
set => SetAndRaise(IsGhostStyleProperty, ref _isGhostStyle, value);
}
private CollapseTriggerType _triggerType = CollapseTriggerType.Header;
internal CollapseTriggerType TriggerType
{
get => _triggerType;
set => SetAndRaise(TriggerTypeProperty, ref _triggerType, value);
}
private CollapseExpandIconPosition _expandIconPosition = CollapseExpandIconPosition.Start;
internal CollapseExpandIconPosition ExpandIconPosition
{
get => _expandIconPosition;
set => SetAndRaise(ExpandIconPositionProperty, ref _expandIconPosition, value);
}
private Thickness _headerBorderThickness;
internal Thickness HeaderBorderThickness
{
get => _headerBorderThickness;
set => SetAndRaise(HeaderBorderThicknessProperty, ref _headerBorderThickness, value);
}
private Thickness _contentBorderThickness;
internal Thickness ContentBorderThickness
{
get => _contentBorderThickness;
set => SetAndRaise(ContentBorderThicknessProperty, ref _contentBorderThickness, value);
}
#endregion
static CollapseItem()
{
SelectableMixin.Attach<CollapseItem>(IsSelectedProperty);
PressedMixin.Attach<CollapseItem>();
FocusableProperty.OverrideDefaultValue(typeof(CollapseItem), true);
DataContextProperty.Changed.AddClassHandler<CollapseItem>((x, e) => x.UpdateHeader(e));
AutomationProperties.ControlTypeOverrideProperty.OverrideDefaultValue<TabItem>(AutomationControlType.TabItem);
}
protected override AutomationPeer OnCreateAutomationPeer() => new ListItemAutomationPeer(this);
private void UpdateHeader(AvaloniaPropertyChangedEventArgs obj)
{
if (Header == null) {
if (obj.NewValue is IHeadered headered) {
if (Header != headered.Header) {
SetCurrentValue(HeaderProperty, headered.Header);
}
} else {
if (!(obj.NewValue is Control)) {
SetCurrentValue(HeaderProperty, obj.NewValue);
}
}
} else {
if (Header == obj.OldValue) {
SetCurrentValue(HeaderProperty, obj.NewValue);
}
}
}
protected override void OnApplyTemplate(TemplateAppliedEventArgs e)
{
base.OnApplyTemplate(e);
SetupIconButton();
HandleSelectedChanged();
}
protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change)
{
base.OnPropertyChanged(change);
if (change.Property == ExpandIconProperty) {
SetupIconButton();
}
if (VisualRoot is not null) {
if (change.Property == IsSelectedProperty) {
HandleSelectedChanged();
}
}
}
private void HandleSelectedChanged()
{
if (Presenter is not null) {
Presenter.IsVisible = IsSelected;
}
}
private void SetupIconButton()
{
if (ExpandIcon is null) {
ExpandIcon = new PathIcon()
{
Kind = "RightOutlined"
};
}
UIStructureUtils.SetTemplateParent(ExpandIcon, this);
}
}

View File

@ -0,0 +1,210 @@
using AtomUI.Theme;
using AtomUI.Theme.Styling;
using AtomUI.Utils;
using Avalonia.Controls;
using Avalonia.Controls.Documents;
using Avalonia.Controls.Presenters;
using Avalonia.Controls.Templates;
using Avalonia.Input;
using Avalonia.Layout;
using Avalonia.Styling;
namespace AtomUI.Controls;
[ControlThemeProvider]
internal class CollapseItemTheme : BaseControlTheme
{
public const string MainLayoutPart = "PART_MainLayout";
public const string ExpandButtonPart = "PART_ExpandButton";
public const string HeaderPresenterPart = "PART_HeaderPresenter";
public const string HeaderDecoratorPart = "PART_HeaderDecorator";
public const string ContentPresenterPart = "PART_ContentPresenter";
public CollapseItemTheme() : base(typeof(CollapseItem)) {}
protected override IControlTemplate BuildControlTemplate()
{
return new FuncControlTemplate<CollapseItem>((collapseItem, scope) =>
{
var mainLayout = new DockPanel()
{
Name = MainLayoutPart,
LastChildFill = true,
};
BuildHeader(mainLayout, scope);
var contentPresenter = new ContentPresenter()
{
Name = ContentPresenterPart,
};
TokenResourceBinder.CreateGlobalTokenBinding(contentPresenter, ContentPresenter.BorderBrushProperty, GlobalResourceKey.ColorBorder);
CreateTemplateParentBinding(contentPresenter, ContentPresenter.ContentProperty, CollapseItem.ContentProperty);
CreateTemplateParentBinding(contentPresenter, ContentPresenter.ContentTemplateProperty, CollapseItem.ContentTemplateProperty);
CreateTemplateParentBinding(contentPresenter, ContentPresenter.BorderThicknessProperty, CollapseItem.ContentBorderThicknessProperty);
mainLayout.Children.Add(contentPresenter);
contentPresenter.RegisterInNameScope(scope);
return mainLayout;
});
}
private void BuildHeader(DockPanel layout, INameScope scope)
{
var headerDecorator = new Border()
{
Name = HeaderDecoratorPart
};
DockPanel.SetDock(headerDecorator, Dock.Top);
TokenResourceBinder.CreateGlobalTokenBinding(headerDecorator, Border.BorderBrushProperty, GlobalResourceKey.ColorBorder);
CreateTemplateParentBinding(headerDecorator, Border.BorderThicknessProperty, CollapseItem.HeaderBorderThicknessProperty);
var headerLayout = new Grid()
{
ColumnDefinitions = new ColumnDefinitions()
{
new ColumnDefinition(GridLength.Auto),
new ColumnDefinition(GridLength.Star),
new ColumnDefinition(GridLength.Auto),
new ColumnDefinition(GridLength.Auto)
}
};
var expandButton = new IconButton()
{
Name = ExpandButtonPart,
VerticalAlignment = VerticalAlignment.Center
};
CreateTemplateParentBinding(expandButton, IconButton.IconProperty, CollapseItem.ExpandIconProperty);
headerLayout.Children.Add(expandButton);
var headerPresenter = new ContentPresenter()
{
Name = HeaderPresenterPart,
HorizontalAlignment = HorizontalAlignment.Stretch,
HorizontalContentAlignment = HorizontalAlignment.Left,
};
CreateTemplateParentBinding(headerPresenter, ContentPresenter.ContentProperty, CollapseItem.HeaderProperty);
CreateTemplateParentBinding(headerPresenter, ContentPresenter.ContentTemplateProperty, CollapseItem.HeaderTemplateProperty);
Grid.SetColumn(headerPresenter, 1);
headerPresenter.RegisterInNameScope(scope);
headerLayout.Children.Add(headerPresenter);
headerDecorator.Child = headerLayout;
layout.Children.Add(headerDecorator);
}
protected override void BuildStyles()
{
BuildCommonStyle();
BuildTriggerStyle();
BuildTriggerPositionStyle();
BuildSizeTypeStyle();
}
private void BuildCommonStyle()
{
var commonStyle = new Style(selector => selector.Nesting());
var decoratorStyle = new Style(selector => selector.Nesting().Template().Name(HeaderDecoratorPart));
decoratorStyle.Add(Border.BackgroundProperty, CollapseResourceKey.HeaderBg);
commonStyle.Add(decoratorStyle);
var headerPresenter = new Style(selector => selector.Nesting().Template().Name(HeaderPresenterPart));
headerPresenter.Add(ContentPresenter.ForegroundProperty, GlobalResourceKey.ColorTextHeading);
commonStyle.Add(headerPresenter);
// ExpandIcon
var expandIconStyle = new Style(selector => selector.Nesting().Template().Name(ExpandButtonPart).Descendant().OfType<PathIcon>());
expandIconStyle.Add(PathIcon.WidthProperty, GlobalResourceKey.IconSizeSM);
expandIconStyle.Add(PathIcon.HeightProperty, GlobalResourceKey.IconSizeSM);
commonStyle.Add(expandIconStyle);
Add(commonStyle);
}
private void BuildSizeTypeStyle()
{
var largeSizeStyle = new Style(selector => selector.Nesting().PropertyEquals(CollapseItem.SizeTypeProperty, SizeType.Large));
{
var decoratorStyle = new Style(selector => selector.Nesting().Template().Name(HeaderDecoratorPart));
decoratorStyle.Add(Border.PaddingProperty, CollapseResourceKey.CollapseHeaderPaddingLG);
decoratorStyle.Add(TextElement.FontSizeProperty, GlobalResourceKey.FontSizeLG);
decoratorStyle.Add(TextBlock.LineHeightProperty, GlobalResourceKey.FontHeightLG);
largeSizeStyle.Add(decoratorStyle);
}
{
var contentPresenterStyle = new Style(selector => selector.Nesting().Template().Name(ContentPresenterPart));
contentPresenterStyle.Add(ContentPresenter.PaddingProperty, CollapseResourceKey.CollapseContentPaddingLG);
largeSizeStyle.Add(contentPresenterStyle);
}
Add(largeSizeStyle);
var middleSizeStyle = new Style(selector => selector.Nesting().PropertyEquals(CollapseItem.SizeTypeProperty, SizeType.Middle));
{
var decoratorStyle = new Style(selector => selector.Nesting().Template().Name(HeaderDecoratorPart));
decoratorStyle.Add(Border.PaddingProperty, CollapseResourceKey.HeaderPadding);
decoratorStyle.Add(TextElement.FontSizeProperty, GlobalResourceKey.FontSize);
decoratorStyle.Add(TextBlock.LineHeightProperty, GlobalResourceKey.FontHeight);
middleSizeStyle.Add(decoratorStyle);
}
{
var contentPresenterStyle = new Style(selector => selector.Nesting().Template().Name(ContentPresenterPart));
contentPresenterStyle.Add(ContentPresenter.PaddingProperty, CollapseResourceKey.ContentPadding);
middleSizeStyle.Add(contentPresenterStyle);
}
Add(middleSizeStyle);
var smallSizeStyle = new Style(selector => selector.Nesting().PropertyEquals(CollapseItem.SizeTypeProperty, SizeType.Small));
{
var decoratorStyle = new Style(selector => selector.Nesting().Template().Name(HeaderDecoratorPart));
decoratorStyle.Add(Border.PaddingProperty, CollapseResourceKey.CollapseHeaderPaddingSM);
decoratorStyle.Add(TextElement.FontSizeProperty, GlobalResourceKey.FontSize);
decoratorStyle.Add(TextBlock.LineHeightProperty, GlobalResourceKey.FontHeight);
smallSizeStyle.Add(decoratorStyle);
}
{
var contentPresenterStyle = new Style(selector => selector.Nesting().Template().Name(ContentPresenterPart));
contentPresenterStyle.Add(ContentPresenter.PaddingProperty, CollapseResourceKey.CollapseContentPaddingSM);
smallSizeStyle.Add(contentPresenterStyle);
}
Add(smallSizeStyle);
}
private void BuildTriggerStyle()
{
var headerTriggerHandleStyle = new Style(selector => selector.Nesting().PropertyEquals(CollapseItem.TriggerTypeProperty, CollapseTriggerType.Header));
var headerDecoratorStyle = new Style(selector => selector.Nesting().Template().Name(HeaderDecoratorPart));
headerDecoratorStyle.Add(Border.CursorProperty, new Cursor(StandardCursorType.Hand));
headerTriggerHandleStyle.Add(headerDecoratorStyle);
Add(headerTriggerHandleStyle);
var iconTriggerHandleStyle = new Style(selector => selector.Nesting().PropertyEquals(CollapseItem.TriggerTypeProperty, CollapseTriggerType.Icon));
var expandIconStyle = new Style(selector => selector.Nesting().Template().Name(ExpandButtonPart));
expandIconStyle.Add(IconButton.CursorProperty, new Cursor(StandardCursorType.Hand));
iconTriggerHandleStyle.Add(expandIconStyle);
Add(iconTriggerHandleStyle);
}
private void BuildTriggerPositionStyle()
{
var startPositionStyle = new Style(selector => selector.Nesting().PropertyEquals(CollapseItem.ExpandIconPositionProperty, CollapseExpandIconPosition.Start));
{
var expandButtonStyle = new Style(selector => selector.Nesting().Template().Name(ExpandButtonPart));
expandButtonStyle.Add(Grid.ColumnProperty, 0);
expandButtonStyle.Add(IconButton.MarginProperty, CollapseResourceKey.LeftExpandButtonMargin);
startPositionStyle.Add(expandButtonStyle);
}
Add(startPositionStyle);
var endPositionStyle = new Style(selector => selector.Nesting().PropertyEquals(CollapseItem.ExpandIconPositionProperty, CollapseExpandIconPosition.End));
{
var expandButtonStyle = new Style(selector => selector.Nesting().Template().Name(ExpandButtonPart));
expandButtonStyle.Add(Grid.ColumnProperty, 3);
expandButtonStyle.Add(IconButton.MarginProperty, CollapseResourceKey.RightExpandButtonMargin);
endPositionStyle.Add(expandButtonStyle);
}
Add(endPositionStyle);
}
}

View File

@ -0,0 +1,51 @@
using AtomUI.Theme;
using AtomUI.Theme.Styling;
using Avalonia.Controls;
using Avalonia.Controls.Presenters;
using Avalonia.Controls.Templates;
using Avalonia.Styling;
namespace AtomUI.Controls;
[ControlThemeProvider]
internal class CollapseTheme : BaseControlTheme
{
public const string FrameDecoratorPart = "PART_FrameDecorator";
public const string ItemsPresenterPart = "PART_ItemsPresenter";
public CollapseTheme() : base(typeof(Collapse)) {}
protected override IControlTemplate BuildControlTemplate()
{
return new FuncControlTemplate<Collapse>((collapse, scope) =>
{
var frameDecorator = new Border()
{
Name = FrameDecoratorPart,
ClipToBounds = true
};
var itemsPresenter = new ItemsPresenter()
{
Name = ItemsPresenterPart
};
itemsPresenter.RegisterInNameScope(scope);
frameDecorator.Child = itemsPresenter;
CreateTemplateParentBinding(itemsPresenter, ItemsPresenter.ItemsPanelProperty, Collapse.ItemsPanelProperty);
CreateTemplateParentBinding(frameDecorator, Border.BorderThicknessProperty, Collapse.EffectiveBorderThicknessProperty);
CreateTemplateParentBinding(frameDecorator, Border.BorderBrushProperty, Collapse.BorderBrushProperty);
CreateTemplateParentBinding(frameDecorator, Border.CornerRadiusProperty, Collapse.CornerRadiusProperty);
return frameDecorator;
});
}
protected override void BuildStyles()
{
var commonStyle = new Style(selector => selector.Nesting());
commonStyle.Add(Collapse.BorderBrushProperty, GlobalResourceKey.ColorBorder);
commonStyle.Add(Collapse.CornerRadiusProperty, CollapseResourceKey.CollapsePanelBorderRadius);
Add(commonStyle);
}
}

View File

@ -0,0 +1,64 @@
using AtomUI.Theme.TokenSystem;
using Avalonia;
using Avalonia.Media;
namespace AtomUI.Controls;
[ControlDesignToken]
internal class CollapseToken : AbstractControlDesignToken
{
public const string ID = "Collapse";
public CollapseToken()
: base(ID)
{
}
/// <summary>
/// 折叠面板头部内边距
/// </summary>
public Thickness HeaderPadding { get; set; }
/// <summary>
/// 折叠面板头部背景
/// </summary>
public Color HeaderBg { get; set; }
/// <summary>
/// 折叠面板内容内边距
/// </summary>
public Thickness ContentPadding { get; set; }
/// <summary>
/// 折叠面板内容背景
/// </summary>
public Color ContentBg { get; set; }
#region Token
public Thickness CollapseHeaderPaddingSM { get; set; }
public Thickness CollapseHeaderPaddingLG { get; set; }
public Thickness CollapseContentPaddingSM { get; set; }
public Thickness CollapseContentPaddingLG { get; set; }
public CornerRadius CollapsePanelBorderRadius { get; set; }
public Thickness LeftExpandButtonMargin { get; set; }
public Thickness RightExpandButtonMargin { get; set; }
#endregion
internal override void CalculateFromAlias()
{
base.CalculateFromAlias();
HeaderPadding = new Thickness(_globalToken.Padding, _globalToken.PaddingSM);
HeaderBg = _globalToken.ColorFillAlter;
ContentPadding = new Thickness(16, _globalToken.Padding);
CollapseContentPaddingSM = new Thickness(_globalToken.PaddingSM);
CollapseContentPaddingLG = new Thickness(_globalToken.PaddingLG);
ContentBg = _globalToken.ColorToken.ColorNeutralToken.ColorBgContainer;
CollapseHeaderPaddingSM = new Thickness(_globalToken.PaddingSM, _globalToken.PaddingXS);
CollapseHeaderPaddingLG = new Thickness(_globalToken.PaddingLG, _globalToken.Padding);
CollapsePanelBorderRadius = _globalToken.StyleToken.BorderRadiusLG;
LeftExpandButtonMargin = new Thickness(0, 0, _globalToken.MarginSM, 0);
RightExpandButtonMargin = new Thickness(_globalToken.MarginSM, 0, 0, 0);
}
}

View File

@ -0,0 +1,6 @@
namespace AtomUI.Controls;
public interface IHeadered
{
object? Header { get; set; }
}

View File

@ -1,7 +1,5 @@
<Project Sdk="Microsoft.NET.Sdk">
<Import Project="$(MSBuildThisFileDirectory)..\..\Build\Common.props" />
<ItemGroup>
<Compile Remove="$(CompilerGeneratedFilesOutputPath)/**/*.cs" />
</ItemGroup>

View File

@ -1,7 +1,5 @@
<Project Sdk="Microsoft.NET.Sdk">
<Import Project="$(MSBuildThisFileDirectory)..\..\Build\Common.props" />
<ItemGroup>
<PackageReference Include="Avalonia"/>
</ItemGroup>

View File

@ -1,7 +1,5 @@
<Project Sdk="Microsoft.NET.Sdk">
<Import Project="$(MSBuildThisFileDirectory)..\..\Build\Common.props" />
<PropertyGroup>
<EmitCompilerGeneratedFiles>true</EmitCompilerGeneratedFiles>
<CompilerGeneratedFilesOutputPath>GeneratedFiles</CompilerGeneratedFilesOutputPath>