完成 Popup 菜单分割控件

This commit is contained in:
polarboy 2024-07-25 11:36:43 +08:00
parent 198a40617c
commit 44d61f86b5
17 changed files with 248 additions and 73 deletions

View File

@ -23,6 +23,9 @@
<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.22" />
<!-- 源码生成 -->
<PackageVersion Include="Microsoft.CodeAnalysis.CSharp" Version="4.10.0-3.final" />

View File

@ -24,6 +24,7 @@
<PackageReference Include="Avalonia.Themes.Fluent" />
<PackageReference Include="Avalonia.Controls.DataGrid" />
<PackageReference Condition="'$(Configuration)' == 'Debug'" Include="Avalonia.Diagnostics" />
<PackageReference Condition="'$(Configuration)' == 'Debug'" Include="Nlnet.Avalonia.DevTools" />
<PackageReference Include="CommunityToolkit.Mvvm" />
</ItemGroup>

View File

@ -3,6 +3,7 @@ using AtomUI.Icon.AntDesign;
using Avalonia;
using Avalonia.Dialogs;
using Avalonia.Media;
using Nlnet.Avalonia.DevTools;
namespace AtomUI.Demo.Desktop;
@ -30,6 +31,7 @@ class Program
.UseManagedSystemDialogs()
.UsePlatformDetect()
.UseAtomUI()
.UseDevToolsForAvalonia()
.UseIconPackage<AntDesignIconPackage>(true)
.With(new Win32PlatformOptions())
.LogToTrace();

View File

@ -126,6 +126,8 @@ public class Alert : TemplatedControl, IControlCustomStyle
_customStyle.HandleTemplateApplied(e.NameScope);
}
#region IControlCustomStyle
void IControlCustomStyle.HandleTemplateApplied(INameScope scope)
{
BindUtils.CreateTokenBinding(this, BorderThicknessProperty, GlobalResourceKey.BorderThickness,
@ -134,8 +136,6 @@ public class Alert : TemplatedControl, IControlCustomStyle
SetupCloseButton();
}
#region IControlCustomStyle
void IControlCustomStyle.HandlePropertyChangedForStyle(AvaloniaPropertyChangedEventArgs e)
{
if (VisualRoot is not null) {

View File

@ -118,15 +118,14 @@ namespace AtomUI.Styling
{
public static readonly TokenResourceKey MenuPopupBorderRadius = new TokenResourceKey("Menu.MenuPopupBorderRadius");
public static readonly TokenResourceKey MenuPopupBoxShadows = new TokenResourceKey("Menu.MenuPopupBoxShadows");
public static readonly TokenResourceKey MenuPopupContentPadding = new TokenResourceKey("Menu.MenuPopupContentPadding");
public static readonly TokenResourceKey MenuPopupMinWidth = new TokenResourceKey("Menu.MenuPopupMinWidth");
public static readonly TokenResourceKey MenuPopupMaxWidth = new TokenResourceKey("Menu.MenuPopupMaxWidth");
public static readonly TokenResourceKey MenuPopupMinHeight = new TokenResourceKey("Menu.MenuPopupMinHeight");
public static readonly TokenResourceKey MenuPopupMaxHeight = new TokenResourceKey("Menu.MenuPopupMaxHeight");
public static readonly TokenResourceKey MenuArrowSize = new TokenResourceKey("Menu.MenuArrowSize");
public static readonly TokenResourceKey MenuArrowOffset = new TokenResourceKey("Menu.MenuArrowOffset");
public static readonly TokenResourceKey MenuMargin = new TokenResourceKey("Menu.MenuMargin");
public static readonly TokenResourceKey MenuTearOffHeight = new TokenResourceKey("Menu.MenuTearOffHeight");
public static readonly TokenResourceKey MenuContentPadding = new TokenResourceKey("Menu.MenuContentPadding");
public static readonly TokenResourceKey MenuBgColor = new TokenResourceKey("Menu.MenuBgColor");
public static readonly TokenResourceKey ItemColor = new TokenResourceKey("Menu.ItemColor");
public static readonly TokenResourceKey ItemHoverColor = new TokenResourceKey("Menu.ItemHoverColor");
@ -159,6 +158,7 @@ namespace AtomUI.Styling
public static readonly TokenResourceKey TopLevelItemLineHeightLG = new TokenResourceKey("Menu.TopLevelItemLineHeightLG");
public static readonly TokenResourceKey TopLevelItemLineHeightSM = new TokenResourceKey("Menu.TopLevelItemLineHeightSM");
public static readonly TokenResourceKey TopLevelItemPopupMarginToAnchor = new TokenResourceKey("Menu.TopLevelItemPopupMarginToAnchor");
public static readonly TokenResourceKey SeparatorItemHeight = new TokenResourceKey("Menu.SeparatorItemHeight");
}
public static class OptionButtonResourceKey

View File

@ -1,12 +1,28 @@
using AtomUI.Styling;
using Avalonia;
using Avalonia.Controls;
using Avalonia.Controls.Templates;
using Avalonia.Media;
namespace AtomUI.Controls;
[ControlThemeProvider]
public class MenuItemTheme : ControlTheme
internal class MenuItemTheme : ControlTheme
{
public MenuItemTheme()
: base(typeof(MenuItem))
{
}
protected override IControlTemplate? BuildControlTemplate()
{
return new FuncControlTemplate<MenuItem>((item, scope) =>
{
return new Border()
{
Height = 30,
Background = new SolidColorBrush(Colors.Chocolate),
};
});
}
}

View File

@ -1,8 +1,42 @@
namespace AtomUI.Controls;
using AtomUI.Styling;
using Avalonia.Controls;
using Avalonia.Controls.Metadata;
using Avalonia.Controls.Presenters;
using Avalonia.Controls.Primitives;
namespace AtomUI.Controls;
using AvaloniaScrollViewer = Avalonia.Controls.ScrollViewer;
public class MenuScrollViewer : AvaloniaScrollViewer
[TemplatePart(MenuScrollViewerTheme.ScrollDownButtonPart, typeof(IconButton))]
[TemplatePart(MenuScrollViewerTheme.ScrollUpButtonPart, typeof(IconButton))]
[TemplatePart(MenuScrollViewerTheme.ScrollViewContentPart, typeof(ScrollContentPresenter))]
public class MenuScrollViewer : AvaloniaScrollViewer, IControlCustomStyle
{
private readonly IControlCustomStyle _customStyle;
private IconButton? _scrollUpButton;
private IconButton? _scrollDownButton;
private ScrollContentPresenter? _scrollViewContent;
public MenuScrollViewer()
{
_customStyle = this;
}
protected override void OnApplyTemplate(TemplateAppliedEventArgs e)
{
base.OnApplyTemplate(e);
_customStyle.HandleTemplateApplied(e.NameScope);
}
#region IControlCustomStyle
void IControlCustomStyle.HandleTemplateApplied(INameScope scope)
{
_scrollUpButton = scope.Find<IconButton>(MenuScrollViewerTheme.ScrollUpButtonPart);
_scrollDownButton = scope.Find<IconButton>(MenuScrollViewerTheme.ScrollDownButtonPart);
_scrollViewContent = scope.Find<ScrollContentPresenter>(MenuScrollViewerTheme.ScrollViewContentPart);
}
#endregion
}

View File

@ -1,12 +1,82 @@
using AtomUI.Styling;
using AtomUI.Utils;
using Avalonia.Controls;
using Avalonia.Controls.Presenters;
using Avalonia.Controls.Templates;
using Avalonia.Input.GestureRecognizers;
namespace AtomUI.Controls;
[ControlThemeProvider]
public class MenuScrollViewerTheme : ControlTheme
internal class MenuScrollViewerTheme : ControlTheme
{
public const string ScrollUpButtonPart = "Part_ScrollUpButton";
public const string ScrollDownButtonPart = "Part_ScrollDownButton";
public const string ScrollViewContentPart = "PART_ContentPresenter";
public const string MainContainerPart = "Part_MainContainer";
public MenuScrollViewerTheme()
: base(typeof(MenuScrollViewer))
: base(typeof(MenuScrollViewer)) { }
protected override IControlTemplate BuildControlTemplate()
{
return new FuncControlTemplate<MenuScrollViewer>((viewer, scope) =>
{
var dockPanel = new DockPanel();
var scrollUpButton = new IconButton()
{
Name = ScrollUpButtonPart,
};
DockPanel.SetDock(scrollUpButton, Dock.Top);
var scrollDownButton = new IconButton()
{
Name = ScrollDownButtonPart
};
DockPanel.SetDock(scrollDownButton, Dock.Bottom);
var scrollViewContent = CreateScrollContentPresenter(viewer);
dockPanel.Children.Add(scrollUpButton);
dockPanel.Children.Add(scrollDownButton);
dockPanel.Children.Add(scrollViewContent);
scrollUpButton.RegisterInNameScope(scope);
scrollDownButton.RegisterInNameScope(scope);
scrollViewContent.RegisterInNameScope(scope);
return dockPanel;
});
}
private ScrollContentPresenter CreateScrollContentPresenter(MenuScrollViewer viewer)
{
var scrollViewContent = new ScrollContentPresenter()
{
Name = ScrollViewContentPart
};
CreateTemplateParentBinding(scrollViewContent, ScrollContentPresenter.MarginProperty,
MenuScrollViewer.PaddingProperty);
CreateTemplateParentBinding(scrollViewContent, ScrollContentPresenter.HorizontalSnapPointsAlignmentProperty,
MenuScrollViewer.HorizontalSnapPointsAlignmentProperty);
CreateTemplateParentBinding(scrollViewContent, ScrollContentPresenter.HorizontalSnapPointsTypeProperty,
MenuScrollViewer.HorizontalSnapPointsTypeProperty);
CreateTemplateParentBinding(scrollViewContent, ScrollContentPresenter.VerticalSnapPointsAlignmentProperty,
MenuScrollViewer.VerticalSnapPointsAlignmentProperty);
CreateTemplateParentBinding(scrollViewContent, ScrollContentPresenter.VerticalSnapPointsTypeProperty,
MenuScrollViewer.VerticalSnapPointsTypeProperty);
var scrollGestureRecognizer = new ScrollGestureRecognizer();
BindUtils.RelayBind(scrollViewContent, ScrollContentPresenter.CanHorizontallyScrollProperty, scrollGestureRecognizer,
ScrollGestureRecognizer.CanHorizontallyScrollProperty);
BindUtils.RelayBind(scrollViewContent, ScrollContentPresenter.CanVerticallyScrollProperty, scrollGestureRecognizer,
ScrollGestureRecognizer.CanVerticallyScrollProperty);
CreateTemplateParentBinding(scrollGestureRecognizer, ScrollGestureRecognizer.IsScrollInertiaEnabledProperty,
MenuScrollViewer.IsScrollInertiaEnabledProperty);
scrollViewContent.GestureRecognizers.Add(scrollGestureRecognizer);
return scrollViewContent;
}
protected override void BuildStyles()
{
}
}

View File

@ -1,6 +1,16 @@
namespace AtomUI.Controls;
using Avalonia;
using Avalonia.Media;
public class MenuSeparator
namespace AtomUI.Controls;
using AvaloniaSeparator = Avalonia.Controls.Separator;
public class MenuSeparator : AvaloniaSeparator
{
public override void Render(DrawingContext context)
{
var linePen = new Pen(BorderBrush);
var offsetY = Bounds.Height / 2.0;
context.DrawLine(linePen, new Point(0, offsetY), new Point(Bounds.Right, offsetY));
}
}

View File

@ -1,4 +1,5 @@
using AtomUI.Styling;
using Avalonia.Styling;
namespace AtomUI.Controls;
@ -9,4 +10,12 @@ public class MenuSeparatorTheme : ControlTheme
: base(typeof(MenuSeparator))
{
}
protected override void BuildStyles()
{
var commonStyle = new Style(selector => selector.Nesting());
commonStyle.Add(MenuSeparator.MinHeightProperty, MenuResourceKey.SeparatorItemHeight);
commonStyle.Add(MenuSeparator.BorderBrushProperty, GlobalResourceKey.ColorBorder);
Add(commonStyle);
}
}

View File

@ -53,8 +53,7 @@ public class MenuTheme : ControlTheme
protected override void BuildStyles()
{
var commonStyle = new Style(selector => selector.Nesting());
commonStyle.Add(Menu.BackgroundProperty, GlobalResourceKey.ColorBgContainer);
commonStyle.Add(Menu.PaddingProperty, new Thickness(0));
commonStyle.Add(Menu.BackgroundProperty, MenuResourceKey.MenuBgColor);
commonStyle.Add(Menu.BorderBrushProperty, GlobalResourceKey.ColorBorder);
var largeSizeType = new Style(selector => selector.Nesting().PropertyEquals(Menu.SizeTypeProperty, SizeType.Large));
largeSizeType.Add(Menu.MinHeightProperty, GlobalResourceKey.ControlHeightLG);

View File

@ -25,6 +25,11 @@ internal class MenuToken : AbstractControlDesignToken
/// </summary>
public BoxShadows MenuPopupBoxShadows { get; set; }
/// <summary>
/// 菜单内容边距
/// </summary>
public Thickness MenuPopupContentPadding { get; set; }
/// <summary>
/// 菜单 Popup 最小宽度
/// </summary>
@ -55,22 +60,12 @@ internal class MenuToken : AbstractControlDesignToken
/// </summary>
public double MenuArrowOffset { get; set; }
/// <summary>
/// 菜单间距
/// </summary>
public Thickness MenuMargin { get; set; }
/// <summary>
/// 分离菜单项的高度,这个用于菜单中快捷功能的图标显示
/// TODO 暂时还没实现,但是最终会实现
/// </summary>
public double MenuTearOffHeight { get; set; }
/// <summary>
/// 菜单内容边距
/// </summary>
public double MenuContentPadding { get; set; }
/// <summary>
/// 弹出框背景色
/// </summary>
@ -231,6 +226,10 @@ internal class MenuToken : AbstractControlDesignToken
/// </summary>
public double TopLevelItemPopupMarginToAnchor { get; set; }
/// <summary>
/// 菜单分割项的高度
/// </summary>
public double SeparatorItemHeight { get; set; }
internal override void CalculateFromAlias()
{
@ -271,13 +270,6 @@ internal class MenuToken : AbstractControlDesignToken
ItemPaddingInline = new Thickness(padding);
ItemIconSize = fontSize;
ItemIconMarginInlineEnd = controlHeightSM - fontSize;
MenuArrowSize = (fontSize / 7.0) * 5.0;
MenuArrowOffset = MenuArrowSize * 0.5;
MenuTearOffHeight = ItemHeight * 1.2; // 暂时这么定义吧
MenuContentPadding = _globalToken.PaddingXXS / 2; // 先默认一个最小的内容间距
MenuMargin = new Thickness(1);
MenuPopupBoxShadows = _globalToken.BoxShadowsSecondary;
TopLevelItemColor = colorNeutralToken.ColorText;
TopLevelItemSelectedColor = colorNeutralToken.ColorTextSecondary;
@ -322,5 +314,13 @@ internal class MenuToken : AbstractControlDesignToken
MenuPopupMinHeight = ItemHeight * 3;
MenuPopupMaxHeight = ItemHeight * 30;
SeparatorItemHeight = _globalToken.SeedToken.LineWidth * 5; // 上下两像素,留一像素给自己
MenuArrowSize = (fontSize / 7.0) * 5.0;
MenuArrowOffset = MenuArrowSize * 0.5;
MenuTearOffHeight = ItemHeight * 1.2; // 暂时这么定义吧
MenuPopupContentPadding = new Thickness(_globalToken.PaddingXXS, MenuPopupBorderRadius.TopLeft / 2);
MenuPopupBoxShadows = _globalToken.BoxShadowsSecondary;
}
}

View File

@ -1,13 +1,11 @@
using AtomUI.Styling;
using AtomUI.Utils;
using Avalonia;
using Avalonia.Controls;
using Avalonia.Controls.Presenters;
using Avalonia.Controls.Templates;
using Avalonia.Data;
using Avalonia.Input;
using Avalonia.Layout;
using Avalonia.Media;
using Avalonia.Styling;
namespace AtomUI.Controls;
@ -19,6 +17,7 @@ public class TopLevelMenuItemTheme : ControlTheme
public const string PopupPart = "PART_Popup";
public const string HeaderPresenterPart = "PART_HeaderPresenter";
public const string ItemsPresenterPart = "PART_ItemsPresenter";
public TopLevelMenuItemTheme() : base(typeof(MenuItem)) {}
@ -59,6 +58,7 @@ public class TopLevelMenuItemTheme : ControlTheme
private Popup CreateMenuPopup(MenuItem menuItem)
{
var popup = new Popup()
{
Name = PopupPart,
@ -66,8 +66,31 @@ public class TopLevelMenuItemTheme : ControlTheme
IsLightDismissEnabled = true,
Placement = PlacementMode.BottomEdgeAlignedLeft,
OverlayInputPassThroughElement = menuItem,
Child = new Border()
};
var border = new Border();
BindUtils.CreateTokenBinding(border, Border.BackgroundProperty, GlobalResourceKey.ColorBgContainer);
BindUtils.CreateTokenBinding(border, Border.CornerRadiusProperty, MenuResourceKey.MenuPopupBorderRadius);
BindUtils.CreateTokenBinding(border, Border.MinWidthProperty, MenuResourceKey.MenuPopupMinWidth);
BindUtils.CreateTokenBinding(border, Border.MaxWidthProperty, MenuResourceKey.MenuPopupMaxWidth);
BindUtils.CreateTokenBinding(border, Border.MinHeightProperty, MenuResourceKey.MenuPopupMinHeight);
BindUtils.CreateTokenBinding(border, Border.MaxHeightProperty, MenuResourceKey.MenuPopupMaxHeight);
BindUtils.CreateTokenBinding(border, Border.PaddingProperty, MenuResourceKey.MenuPopupContentPadding);
var scrollViewer = new MenuScrollViewer();
var itemsPresenter = new ItemsPresenter()
{
Name = ItemsPresenterPart,
};
CreateTemplateParentBinding(itemsPresenter, ItemsPresenter.ItemsPanelProperty, MenuItem.ItemsPanelProperty);
Grid.SetIsSharedSizeScope(itemsPresenter, true);
scrollViewer.Content = itemsPresenter;
border.Child = scrollViewer;
popup.Child = border;
BindUtils.CreateTokenBinding(popup, Popup.MarginToAnchorProperty, MenuResourceKey.TopLevelItemPopupMarginToAnchor);
BindUtils.CreateTokenBinding(popup, Popup.MaskShadowsProperty, MenuResourceKey.MenuPopupBoxShadows);
CreateTemplateParentBinding(popup, Popup.IsOpenProperty, MenuItem.IsSubMenuOpenProperty, BindingMode.TwoWay);
return popup;
}
@ -77,7 +100,6 @@ public class TopLevelMenuItemTheme : ControlTheme
BuildCommonStyle();
BuildSizeTypeStyle();
BuildDisabledStyle();
BuildPopupStyle();
}
private void BuildCommonStyle()
@ -132,25 +154,6 @@ public class TopLevelMenuItemTheme : ControlTheme
Add(smallSizeStyle);
}
private void BuildPopupStyle()
{
var popupStyle = new Style(selector => selector.Nesting().Template().OfType<Popup>());
popupStyle.Add(Popup.MarginToAnchorProperty, MenuResourceKey.TopLevelItemPopupMarginToAnchor);
popupStyle.Add(Popup.MaskShadowsProperty, MenuResourceKey.MenuPopupBoxShadows);
Add(popupStyle);
var borderStyle = new Style(selector => selector.Nesting().Template().OfType<Popup>().Child().OfType<Border>());
borderStyle.Add(Border.BackgroundProperty, GlobalResourceKey.ColorBgContainer);
borderStyle.Add(Border.CornerRadiusProperty, MenuResourceKey.MenuPopupBorderRadius);
borderStyle.Add(Border.MinWidthProperty, MenuResourceKey.MenuPopupMinWidth);
borderStyle.Add(Border.MaxWidthProperty, MenuResourceKey.MenuPopupMaxWidth);
borderStyle.Add(Border.MinHeightProperty, MenuResourceKey.MenuPopupMinHeight);
borderStyle.Add(Border.MaxHeightProperty, MenuResourceKey.MenuPopupMaxHeight);
Add(borderStyle);
}
private void BuildDisabledStyle()
{
var disabledStyle = new Style(selector => selector.Nesting().Class(StdPseudoClass.Disabled));

View File

@ -7,14 +7,14 @@ using Avalonia.Controls;
using Avalonia.Controls.Primitives;
using Avalonia.Data;
using Avalonia.Layout;
using Avalonia.LogicalTree;
using Avalonia.Markup.Xaml.Templates;
using Avalonia.Media;
using Avalonia.Media.Imaging;
using Avalonia.Metadata;
namespace AtomUI.Controls;
using AvaloniaSeparator = Avalonia.Controls.Separator;
public enum SeparatorTitlePosition
{
Left,
@ -22,7 +22,7 @@ public enum SeparatorTitlePosition
Center
}
public partial class Separator : TemplatedControl, IControlCustomStyle
public class Separator : AvaloniaSeparator, IControlCustomStyle
{
#region

View File

@ -10,7 +10,7 @@ namespace AtomUI.Controls;
[ControlThemeProvider]
internal class SeparatorTheme : ControlTheme
{
public const string TitlePart = "PART_CloseBtn";
public const string TitlePart = "PART_Title";
public SeparatorTheme()
: base(typeof(Separator)) { }

View File

@ -1,5 +1,4 @@
using Avalonia;
using Avalonia.Controls;
using Avalonia.Controls.Primitives;
using Avalonia.Controls.Templates;
using Avalonia.Data;
@ -22,6 +21,7 @@ public class ControlTheme : AvaloniaControlTheme
if (template is not null) {
Add(new Setter(TemplatedControl.TemplateProperty, template));
}
NotifyBuildCompleted();
}
@ -30,24 +30,52 @@ public class ControlTheme : AvaloniaControlTheme
return default;
}
protected virtual IControlTemplate? BuildControlTemplate() { return default; }
protected virtual IControlTemplate? BuildControlTemplate()
{
return default;
}
protected virtual void BuildStyles() { }
protected virtual void NotifyPreBuild() { }
protected virtual void NotifyBuildCompleted() { }
protected static IDisposable CreateTemplateParentBinding(Control control, AvaloniaProperty property, string templateParentPath,
protected static IDisposable CreateTemplateParentBinding(AvaloniaObject target, AvaloniaProperty property,
string templateParentPath,
BindingMode mode = BindingMode.Default)
{
return control.Bind(property, new Binding(templateParentPath)
return target.Bind(property, new Binding(templateParentPath)
{
RelativeSource = new RelativeSource(RelativeSourceMode.TemplatedParent),
Mode = mode
});
}
protected static IDisposable CreateTemplateParentBinding(Control control, AvaloniaProperty property, AvaloniaProperty templateParentProperty,
protected static IDisposable CreateTemplateParentBinding<T>(AvaloniaObject target, StyledProperty<T> property,
string templateParentPath,
BindingMode mode = BindingMode.Default)
{
return CreateTemplateParentBinding(control, property, templateParentProperty.Name, mode);
return target.Bind(property, new Binding(templateParentPath)
{
RelativeSource = new RelativeSource(RelativeSourceMode.TemplatedParent),
Mode = mode
});
}
protected static IDisposable CreateTemplateParentBinding<T>(AvaloniaObject target, DirectPropertyBase<T> property,
string templateParentPath,
BindingMode mode = BindingMode.Default)
{
return target.Bind(property, new Binding(templateParentPath)
{
RelativeSource = new RelativeSource(RelativeSourceMode.TemplatedParent),
Mode = mode
});
}
protected static IDisposable CreateTemplateParentBinding(AvaloniaObject target, AvaloniaProperty property,
AvaloniaProperty templateParentProperty,
BindingMode mode = BindingMode.Default)
{
return CreateTemplateParentBinding(target, property, templateParentProperty.Name, mode);
}
}

View File

@ -11,7 +11,7 @@ public static class BindUtils
{
public static IDisposable RelayBind(AvaloniaObject source, AvaloniaProperty sourceProperty, AvaloniaObject target,
AvaloniaProperty? targetProperty = null,
BindingMode mode = BindingMode.OneWay)
BindingMode mode = BindingMode.Default)
{
targetProperty ??= sourceProperty;
var registry = AvaloniaPropertyRegistry.Instance;