Complete NavMenu horizontal menu indicator

This commit is contained in:
polarboy
2024-09-22 23:35:28 +08:00
parent fd92dc3a99
commit f6697e48ba
6 changed files with 181 additions and 59 deletions
@@ -193,7 +193,7 @@
Description="Horizontal top navigation menu.">
<atom:NavMenu>
<atom:NavMenuItem Header="Navigation One" Icon="{atom:IconProvider Kind=MailOutlined}" />
<atom:NavMenuItem Header="Navigation Two" Icon="{atom:IconProvider Kind=AppstoreOutlined}" />
<atom:NavMenuItem Header="Navigation Two" Icon="{atom:IconProvider Kind=AppstoreOutlined}" IsEnabled="False"/>
<atom:NavMenuItem Header="Navigation Three - Submenu" Icon="{atom:IconProvider Kind=SettingOutlined}">
<atom:NavMenuItem Header="Item 1">
<atom:NavMenuItem Header="Option 1" />
@@ -396,8 +396,8 @@ namespace AtomUI.Theme.Styling
public static readonly TokenResourceKey HorizontalItemSelectedBg = new TokenResourceKey("NavMenu.HorizontalItemSelectedBg", "AtomUI.Token");
public static readonly TokenResourceKey ActiveBarWidth = new TokenResourceKey("NavMenu.ActiveBarWidth", "AtomUI.Token");
public static readonly TokenResourceKey ActiveBarHeight = new TokenResourceKey("NavMenu.ActiveBarHeight", "AtomUI.Token");
public static readonly TokenResourceKey ActiveBarBorderWidth = new TokenResourceKey("NavMenu.ActiveBarBorderWidth", "AtomUI.Token");
public static readonly TokenResourceKey ItemMargin = new TokenResourceKey("NavMenu.ItemMargin", "AtomUI.Token");
public static readonly TokenResourceKey HorizontalItemMargin = new TokenResourceKey("NavMenu.HorizontalItemMargin", "AtomUI.Token");
public static readonly TokenResourceKey ItemPadding = new TokenResourceKey("NavMenu.ItemPadding", "AtomUI.Token");
public static readonly TokenResourceKey HorizontalItemHoverBg = new TokenResourceKey("NavMenu.HorizontalItemHoverBg", "AtomUI.Token");
public static readonly TokenResourceKey HorizontalItemBorderRadius = new TokenResourceKey("NavMenu.HorizontalItemBorderRadius", "AtomUI.Token");
@@ -423,7 +423,6 @@ namespace AtomUI.Theme.Styling
public static readonly TokenResourceKey DarkDangerItemHoverColor = new TokenResourceKey("NavMenu.DarkDangerItemHoverColor", "AtomUI.Token");
public static readonly TokenResourceKey DarkDangerItemSelectedColor = new TokenResourceKey("NavMenu.DarkDangerItemSelectedColor", "AtomUI.Token");
public static readonly TokenResourceKey DarkDangerItemActiveBg = new TokenResourceKey("NavMenu.DarkDangerItemActiveBg", "AtomUI.Token");
public static readonly TokenResourceKey ItemWidth = new TokenResourceKey("NavMenu.ItemWidth", "AtomUI.Token");
public static readonly TokenResourceKey MenuHorizontalHeight = new TokenResourceKey("NavMenu.MenuHorizontalHeight", "AtomUI.Token");
public static readonly TokenResourceKey MenuArrowSize = new TokenResourceKey("NavMenu.MenuArrowSize", "AtomUI.Token");
public static readonly TokenResourceKey MenuArrowOffset = new TokenResourceKey("NavMenu.MenuArrowOffset", "AtomUI.Token");
+35 -4
View File
@@ -7,6 +7,7 @@ using Avalonia.Automation;
using Avalonia.Automation.Peers;
using Avalonia.Controls;
using Avalonia.Controls.Metadata;
using Avalonia.Controls.Primitives;
using Avalonia.Controls.Templates;
using Avalonia.Data;
using Avalonia.Input;
@@ -31,10 +32,17 @@ public class NavMenu : NavMenuBase
/// Defines the <see cref="Mode"/> property.
/// </summary>
public static readonly StyledProperty<NavMenuMode> ModeProperty =
AvaloniaProperty.Register<NavMenuItem, NavMenuMode>(nameof(Mode), NavMenuMode.Horizontal);
AvaloniaProperty.Register<NavMenu, NavMenuMode>(nameof(Mode), NavMenuMode.Horizontal);
public static readonly StyledProperty<bool> IsDarkStyleProperty =
AvaloniaProperty.Register<NavMenuItem, bool>(nameof(IsDarkStyle), false);
AvaloniaProperty.Register<NavMenu, bool>(nameof(IsDarkStyle), false);
public static readonly StyledProperty<double> ActiveBarWidthProperty =
AvaloniaProperty.Register<NavMenu, double>(nameof(ActiveBarWidth), 1.0d,
coerce: (o, v) => Math.Max(Math.Min(v, 1.0), 0.0));
public static readonly StyledProperty<double> ActiveBarHeightProperty =
AvaloniaProperty.Register<NavMenu, double>(nameof(ActiveBarHeight));
public NavMenuMode Mode
{
@@ -48,6 +56,18 @@ public class NavMenu : NavMenuBase
set => SetValue(IsDarkStyleProperty, value);
}
public double ActiveBarWidth
{
get => GetValue(ActiveBarWidthProperty);
set => SetValue(ActiveBarWidthProperty, value);
}
public double ActiveBarHeight
{
get => GetValue(ActiveBarHeightProperty);
set => SetValue(ActiveBarHeightProperty, value);
}
#endregion
#region 内部属性定义
@@ -146,13 +166,18 @@ public class NavMenu : NavMenuBase
protected override void PrepareContainerForItemOverride(Control element, object? item, int index)
{
base.PrepareContainerForItemOverride(element, item, index);
// Child menu items should not inherit the menu's ItemContainerTheme as that is specific
// for top-level menu items.
if ((element as NavMenuItem)?.ItemContainerTheme == ItemContainerTheme)
{
element.ClearValue(ItemContainerThemeProperty);
}
if (element is NavMenuItem navMenuItem && Mode == NavMenuMode.Horizontal)
{
BindUtils.RelayBind(this, ActiveBarHeightProperty, navMenuItem, NavMenuItem.ActiveBarHeightProperty);
BindUtils.RelayBind(this, ActiveBarWidthProperty, navMenuItem, NavMenuItem.ActiveBarWidthProperty);
}
}
private void UpdatePseudoClasses()
@@ -178,7 +203,13 @@ public class NavMenu : NavMenuBase
BindingPriority.Template,
new RenderScaleAwareDoubleConfigure(this));
}
protected override void OnApplyTemplate(TemplateAppliedEventArgs e)
{
base.OnApplyTemplate(e);
TokenResourceBinder.CreateTokenBinding(this, ActiveBarWidthProperty, NavMenuTokenResourceKey.ActiveBarWidth);
TokenResourceBinder.CreateTokenBinding(this, ActiveBarHeightProperty, NavMenuTokenResourceKey.ActiveBarHeight);
}
private void SetupItemContainerTheme(bool force = false)
{
@@ -208,6 +208,40 @@ public class NavMenuItem : HeaderedSelectingItemsControl,
#endregion
#region 内部属性定义
internal static readonly StyledProperty<double> ActiveBarWidthProperty =
NavMenu.ActiveBarWidthProperty.AddOwner<NavMenuItem>();
internal static readonly StyledProperty<double> ActiveBarHeightProperty =
NavMenu.ActiveBarHeightProperty.AddOwner<NavMenuItem>();
internal static readonly DirectProperty<NavMenuItem, double> EffectiveActiveBarWidthProperty =
AvaloniaProperty.RegisterDirect<NavMenuItem, double>(nameof(EffectiveActiveBarWidth),
o => o.EffectiveActiveBarWidth,
(o, v) => o.EffectiveActiveBarWidth = v);
public double ActiveBarWidth
{
get => GetValue(ActiveBarWidthProperty);
set => SetValue(ActiveBarWidthProperty, value);
}
public double ActiveBarHeight
{
get => GetValue(ActiveBarHeightProperty);
set => SetValue(ActiveBarHeightProperty, value);
}
private double _effectiveActiveBarWidth;
public double EffectiveActiveBarWidth
{
get => _effectiveActiveBarWidth;
set => SetAndRaise(EffectiveActiveBarWidthProperty, ref _effectiveActiveBarWidth, value);
}
#endregion
#region 公共事件定义
/// <summary>
@@ -305,6 +339,7 @@ public class NavMenuItem : HeaderedSelectingItemsControl,
private Popup? _popup;
private KeyGesture? _hotkey;
private bool _isEmbeddedInMenu;
private Border? _horizontalFrame;
static NavMenuItem()
{
@@ -500,6 +535,8 @@ public class NavMenuItem : HeaderedSelectingItemsControl,
_popup.Opened += PopupOpened;
_popup.Closed += PopupClosed;
}
_horizontalFrame = e.NameScope.Find<Border>(TopLevelHorizontalNavMenuItemTheme.FramePart);
}
protected override void UpdateDataValidation(
@@ -643,9 +680,22 @@ public class NavMenuItem : HeaderedSelectingItemsControl,
else if (change.Property == CommandParameterProperty)
{
CommandParameterChanged(change);
} else if (change.Property == BoundsProperty)
{
SetupHorizontalEffectiveIndicatorWidth();
}
}
private void SetupHorizontalEffectiveIndicatorWidth()
{
var width = Bounds.Width;
if (_horizontalFrame != null)
{
width = _horizontalFrame.Bounds.Width;
}
EffectiveActiveBarWidth = ActiveBarWidth * width;
}
/// <summary>
/// Called when the <see cref="HeaderedSelectingItemsControl.Header"/> property changes.
/// </summary>
+15 -18
View File
@@ -129,27 +129,27 @@ internal class NavMenuToken : AbstractControlDesignToken
/// 水平菜单项选中态背景色
/// </summary>
public Color HorizontalItemSelectedBg { get; set; }
/// <summary>
/// 菜单项指示条宽度
/// </summary>
public double ActiveBarWidth { get; set; }
public double ActiveBarWidth { get; set; } = double.NaN;
/// <summary>
/// 菜单项指示条高度
/// </summary>
public double ActiveBarHeight { get; set; }
public double ActiveBarHeight { get; set; } = double.NaN;
/// <summary>
/// 菜单项指示条边框宽度
/// </summary>
public double ActiveBarBorderWidth { get; set; }
/// <summary>
/// 菜单项横向外间距
/// 菜单项外间距
/// </summary>
public Thickness ItemMargin { get; set; }
/// <summary>
/// 水平菜单项外间距
/// </summary>
public Thickness HorizontalItemMargin { get; set; }
/// <summary>
/// 菜单项横向内间距
/// </summary>
@@ -275,9 +275,6 @@ internal class NavMenuToken : AbstractControlDesignToken
/// 暗色模式下的危险菜单项激活态背景
/// </summary>
public Color DarkDangerItemActiveBg { get; set; }
// 内部属性
public double ItemWidth { get; set; }
#region 菜单相关 Token
@@ -314,10 +311,10 @@ internal class NavMenuToken : AbstractControlDesignToken
var colorTextLightSolid = _globalToken.ColorTextLightSolid;
var activeBarWidth = !double.IsNaN(ActiveBarWidth) ? ActiveBarWidth : 0.0d;
var activeBarBorderWidth = !double.IsNaN(ActiveBarBorderWidth)
? ActiveBarBorderWidth
: _globalToken.LineWidth;
var activeBarWidth = !double.IsNaN(ActiveBarWidth) ? ActiveBarWidth : 1.0d;
var activeBarHeight = !double.IsNaN(ActiveBarHeight)
? ActiveBarHeight
: _globalToken.LineWidthBold;
var itemMargin = ItemMargin != default ? ItemMargin : new Thickness(_globalToken.MarginXXS, _globalToken.MarginXXS);
var colorTextDark = ColorUtils.FromRgbF(
0.65d,
@@ -342,8 +339,7 @@ internal class NavMenuToken : AbstractControlDesignToken
ItemSelectedBg = _globalToken.ControlItemBgActive;
HorizontalItemSelectedBg = Colors.Transparent;
ActiveBarWidth = activeBarWidth;
ActiveBarHeight = _globalToken.LineWidthBold;
ActiveBarBorderWidth = activeBarBorderWidth;
ActiveBarHeight = activeBarHeight;
// Disabled
ItemDisabledColor = _globalToken.ColorTextDisabled;
@@ -393,6 +389,7 @@ internal class NavMenuToken : AbstractControlDesignToken
DarkDangerItemActiveBg = _globalToken.ColorError;
MenuHorizontalHeight = _globalToken.ControlHeightLG * 1.15;
HorizontalItemMargin = new Thickness(_globalToken.Padding, 0);
MenuArrowSize = _globalToken.FontSize / 7 * 5;
MenuArrowOffset = MenuArrowSize * 0.25;
MenuSubMenuBg = _globalToken.ColorBgElevated;
@@ -1,13 +1,20 @@
using AtomUI.Theme;
using AtomUI.Icon;
using AtomUI.Media;
using AtomUI.Theme;
using AtomUI.Theme.Styling;
using AtomUI.Theme.Utils;
using AtomUI.Utils;
using Avalonia.Animation;
using Avalonia.Controls;
using Avalonia.Controls.Presenters;
using Avalonia.Controls.Primitives;
using Avalonia.Controls.Shapes;
using Avalonia.Controls.Templates;
using Avalonia.Data;
using Avalonia.Data.Converters;
using Avalonia.Input;
using Avalonia.Layout;
using Avalonia.Media;
using Avalonia.Styling;
namespace AtomUI.Controls;
@@ -21,6 +28,7 @@ internal class TopLevelHorizontalNavMenuItemTheme : BaseControlTheme
public const string HeaderPresenterPart = "PART_HeaderPresenter";
public const string ItemsPresenterPart = "PART_ItemsPresenter";
public const string ItemIconPresenterPart = "PART_ItemIconPresenter";
public const string ActiveIndicatorPart = "PART_ActiveIndicator";
public TopLevelHorizontalNavMenuItemTheme() : base(typeof(NavMenuItem))
{
@@ -37,19 +45,24 @@ internal class TopLevelHorizontalNavMenuItemTheme : BaseControlTheme
{
BuildInstanceStyles(menuItem);
var rootLayout = new Panel();
var frame = new Border()
{
Name = FramePart
Name = FramePart,
HorizontalAlignment = HorizontalAlignment.Stretch,
VerticalAlignment = VerticalAlignment.Stretch
};
frame.RegisterInNameScope(scope);
var rootContainerLayout = new DockPanel()
var contentLayout = new DockPanel()
{
LastChildFill = true
};
var popup = CreateMenuPopup();
popup.RegisterInNameScope(scope);
rootContainerLayout.Children.Add(popup);
contentLayout.Children.Add(popup);
var iconPresenter = new ContentPresenter()
{
@@ -61,10 +74,13 @@ internal class TopLevelHorizontalNavMenuItemTheme : BaseControlTheme
DockPanel.SetDock(iconPresenter, Dock.Left);
CreateTemplateParentBinding(iconPresenter, ContentPresenter.ContentProperty, NavMenuItem.IconProperty);
CreateTemplateParentBinding(iconPresenter, ContentPresenter.IsVisibleProperty, NavMenuItem.IconProperty,
BindingMode.Default,
ObjectConverters.IsNotNull);
TokenResourceBinder.CreateTokenBinding(iconPresenter, Layoutable.MarginProperty,
NavMenuTokenResourceKey.IconMargin);
rootContainerLayout.Children.Add(iconPresenter);
contentLayout.Children.Add(iconPresenter);
var contentPresenter = new ContentPresenter
{
@@ -84,13 +100,32 @@ internal class TopLevelHorizontalNavMenuItemTheme : BaseControlTheme
CreateTemplateParentBinding(contentPresenter, Layoutable.MinHeightProperty, Layoutable.MinHeightProperty);
CreateTemplateParentBinding(contentPresenter, ContentPresenter.FontSizeProperty,
TemplatedControl.FontSizeProperty);
CreateTemplateParentBinding(contentPresenter, ContentPresenter.ForegroundProperty,
TemplatedControl.ForegroundProperty);
contentPresenter.RegisterInNameScope(scope);
rootContainerLayout.Children.Add(contentPresenter);
contentLayout.Children.Add(contentPresenter);
frame.Child = rootContainerLayout;
frame.Child = contentLayout;
return frame;
rootLayout.Children.Add(frame);
var activeIndicator = new Rectangle()
{
Name = ActiveIndicatorPart,
VerticalAlignment = VerticalAlignment.Bottom,
HorizontalAlignment = HorizontalAlignment.Center,
Transitions = new Transitions()
{
AnimationUtils.CreateTransition<SolidColorBrushTransition>(Rectangle.FillProperty)
}
};
CreateTemplateParentBinding(activeIndicator, Rectangle.HeightProperty, NavMenuItem.ActiveBarHeightProperty);
CreateTemplateParentBinding(activeIndicator, Rectangle.WidthProperty, NavMenuItem.EffectiveActiveBarWidthProperty);
rootLayout.Children.Add(activeIndicator);
return rootLayout;
});
}
@@ -100,7 +135,7 @@ internal class TopLevelHorizontalNavMenuItemTheme : BaseControlTheme
{
Name = PopupPart,
WindowManagerAddShadowHint = false,
IsLightDismissEnabled = false,
IsLightDismissEnabled = true,
Placement = PlacementMode.BottomEdgeAlignedLeft
};
@@ -145,45 +180,49 @@ internal class TopLevelHorizontalNavMenuItemTheme : BaseControlTheme
}
protected override void BuildStyles()
{
var topLevelStyle = new Style(selector => selector.Nesting().Class(NavMenuItem.TopLevelPC));
topLevelStyle.Add(NavMenuItem.CursorProperty, new Cursor(StandardCursorType.Hand));
BuildCommonStyle(topLevelStyle);
BuildDisabledStyle(topLevelStyle);
Add(topLevelStyle);
}
private void BuildCommonStyle(Style topLevelStyle)
{
var commonStyle =
new Style(selector => selector.Nesting().PropertyEquals(InputElement.IsEnabledProperty, true));
new Style(selector => selector.Nesting());
commonStyle.Add(TemplatedControl.BackgroundProperty, GlobalTokenResourceKey.ColorTransparent);
// hover 状态
var hoverStyle = new Style(selector => selector.Nesting().Class(StdPseudoClass.PointerOver));
{
}
commonStyle.Add(hoverStyle);
var frameStyle = new Style(selector => selector.Nesting().Template().Name(FramePart));
frameStyle.Add(Border.PaddingProperty, NavMenuTokenResourceKey.ItemPadding);
commonStyle.Add(frameStyle);
commonStyle.Add(NavMenuItem.CursorProperty, new Cursor(StandardCursorType.Hand));
commonStyle.Add(TemplatedControl.FontSizeProperty, GlobalTokenResourceKey.FontSize);
{
var frameStyle = new Style(selector => selector.Nesting().Template().Name(FramePart));
frameStyle.Add(Border.MarginProperty, NavMenuTokenResourceKey.HorizontalItemMargin);
commonStyle.Add(frameStyle);
}
var presenterStyle = new Style(selector => selector.Nesting().Template().Name(HeaderPresenterPart));
presenterStyle.Add(ContentPresenter.LineHeightProperty, NavMenuTokenResourceKey.HorizontalLineHeight);
commonStyle.Add(presenterStyle);
topLevelStyle.Add(commonStyle);
BuildActiveIndicatorStyle(commonStyle);
Add(commonStyle);
BuildDisabledStyle();
}
private void BuildDisabledStyle(Style topLevelStyle)
private void BuildActiveIndicatorStyle(Style commonStyle)
{
{
var indicatorStyle = new Style(selector => selector.Nesting().Template().Name(ActiveIndicatorPart));
indicatorStyle.Add(Rectangle.FillProperty, new SolidColorBrush(Colors.Transparent));
commonStyle.Add(indicatorStyle);
}
var hoverStyle = new Style(selector => Selectors.Or(selector.Nesting().Class(StdPseudoClass.PointerOver),
selector.Nesting().Class(StdPseudoClass.Open)));
{
var indicatorStyle = new Style(selector => selector.Nesting().Template().Name(ActiveIndicatorPart));
indicatorStyle.Add(Rectangle.FillProperty, GlobalTokenResourceKey.ColorPrimary);
hoverStyle.Add(indicatorStyle);
}
commonStyle.Add(hoverStyle);
}
private void BuildDisabledStyle()
{
var disabledStyle = new Style(selector => selector.Nesting().Class(StdPseudoClass.Disabled));
disabledStyle.Add(TemplatedControl.ForegroundProperty, NavMenuTokenResourceKey.ItemDisabledColor);
topLevelStyle.Add(disabledStyle);
Add(disabledStyle);
}
protected override void BuildInstanceStyles(Control control)
@@ -192,6 +231,12 @@ internal class TopLevelHorizontalNavMenuItemTheme : BaseControlTheme
iconStyle.Add(PathIcon.WidthProperty, NavMenuTokenResourceKey.ItemIconSize);
iconStyle.Add(PathIcon.HeightProperty, NavMenuTokenResourceKey.ItemIconSize);
iconStyle.Add(PathIcon.NormalFilledBrushProperty, GlobalTokenResourceKey.ColorText);
iconStyle.Add(PathIcon.DisabledFilledBrushProperty, NavMenuTokenResourceKey.ItemDisabledColor);
iconStyle.Add(PathIcon.SelectedFilledBrushProperty, GlobalTokenResourceKey.ColorPrimary);
control.Styles.Add(iconStyle);
var disabledIconStyle = new Style(selector => selector.OfType<PathIcon>().Class(StdPseudoClass.Disabled));
disabledIconStyle.Add(PathIcon.IconModeProperty, IconMode.Disabled);
control.Styles.Add(disabledIconStyle);
}
}