mirror of
https://github.com/AtomUI/AtomUI.git
synced 2026-10-24 10:12:35 +08:00
445 lines
14 KiB
C#
445 lines
14 KiB
C#
using System.Reflection;
|
|
using Avalonia;
|
|
using Avalonia.Controls;
|
|
using Avalonia.Controls.Primitives;
|
|
using Avalonia.Headless;
|
|
using Avalonia.Threading;
|
|
using Avalonia.VisualTree;
|
|
using Shouldly;
|
|
using Xunit;
|
|
|
|
namespace AtomUI.Desktop.Controls.Tests.NavMenu;
|
|
|
|
public class NavMenuLayoutTests
|
|
{
|
|
static NavMenuLayoutTests()
|
|
{
|
|
AvaloniaTestApp.EnsureInitialized();
|
|
}
|
|
|
|
[Theory]
|
|
[InlineData(NavMenuMode.Inline)]
|
|
[InlineData(NavMenuMode.Vertical)]
|
|
public void Root_Item_Inset_Uses_AntDesign_Item_Margins_Without_Outer_Menu_Padding(NavMenuMode mode)
|
|
{
|
|
var menu = new AtomUI.Desktop.Controls.NavMenu
|
|
{
|
|
Mode = mode,
|
|
IsMotionEnabled = false,
|
|
Width = 240
|
|
};
|
|
var item = new NavMenuNode
|
|
{
|
|
Header = "Item",
|
|
ItemKey = "item"
|
|
};
|
|
menu.Items.Add(item);
|
|
|
|
var window = new Avalonia.Controls.Window
|
|
{
|
|
Width = 320,
|
|
Height = 240,
|
|
Content = menu
|
|
};
|
|
|
|
try
|
|
{
|
|
window.Show();
|
|
Dispatcher.UIThread.RunJobs();
|
|
|
|
var itemContainer = (Control)menu.ContainerFromItem(item)!;
|
|
var itemHeader = GetItemHeader(itemContainer);
|
|
var leftInset = GetLeft(itemHeader, menu);
|
|
var rightInset = menu.Bounds.Width - GetRight(itemHeader, menu);
|
|
var topInset = GetTop(itemHeader, menu);
|
|
|
|
leftInset.ShouldBe(4, 0.5,
|
|
"Ant Design vertical/inline root menus do not add outer menu padding; item marginInline is the only horizontal inset.");
|
|
rightInset.ShouldBe(4, 0.5,
|
|
"Ant Design vertical/inline root menus do not add outer menu padding; item marginInline is the only horizontal inset.");
|
|
topInset.ShouldBe(4, 0.5,
|
|
"Ant Design vertical/inline root menus expose one marginXXS before the first item.");
|
|
}
|
|
finally
|
|
{
|
|
window.Close();
|
|
}
|
|
}
|
|
|
|
[Theory]
|
|
[InlineData(NavMenuMode.Inline)]
|
|
[InlineData(NavMenuMode.Vertical)]
|
|
public void Vertical_Item_Header_Gap_Collapses_AntDesign_Block_Margins(NavMenuMode mode)
|
|
{
|
|
var menu = new AtomUI.Desktop.Controls.NavMenu
|
|
{
|
|
Mode = mode,
|
|
IsMotionEnabled = false,
|
|
Width = 240
|
|
};
|
|
var first = new NavMenuNode
|
|
{
|
|
Header = "First",
|
|
ItemKey = "first"
|
|
};
|
|
var second = new NavMenuNode
|
|
{
|
|
Header = "Second",
|
|
ItemKey = "second"
|
|
};
|
|
menu.Items.Add(first);
|
|
menu.Items.Add(second);
|
|
|
|
var window = new Avalonia.Controls.Window
|
|
{
|
|
Width = 320,
|
|
Height = 240,
|
|
Content = menu
|
|
};
|
|
|
|
try
|
|
{
|
|
window.Show();
|
|
|
|
var firstContainer = (Control)menu.ContainerFromItem(first)!;
|
|
var secondContainer = (Control)menu.ContainerFromItem(second)!;
|
|
var firstHeader = GetItemHeader(firstContainer);
|
|
var secondHeader = GetItemHeader(secondContainer);
|
|
|
|
var gap = GetTop(secondHeader, menu) - GetBottom(firstHeader, menu);
|
|
|
|
gap.ShouldBe(4, 0.5,
|
|
"Ant Design uses marginBlock: marginXXS on adjacent block menu items; CSS collapses those adjacent vertical margins to 4px.");
|
|
}
|
|
finally
|
|
{
|
|
window.Close();
|
|
}
|
|
}
|
|
|
|
[Fact]
|
|
public void Inline_Submenu_First_Child_Header_Gap_Collapses_AntDesign_Block_Margins()
|
|
{
|
|
var menu = new AtomUI.Desktop.Controls.NavMenu
|
|
{
|
|
Mode = NavMenuMode.Inline,
|
|
IsMotionEnabled = false,
|
|
Width = 240
|
|
};
|
|
var child = new NavMenuNode
|
|
{
|
|
Header = "Child",
|
|
ItemKey = "child"
|
|
};
|
|
var parent = new NavMenuNode
|
|
{
|
|
Header = "Parent",
|
|
ItemKey = "parent"
|
|
};
|
|
parent.Children.Add(child);
|
|
menu.Items.Add(parent);
|
|
|
|
var window = new Avalonia.Controls.Window
|
|
{
|
|
Width = 320,
|
|
Height = 320,
|
|
Content = menu
|
|
};
|
|
|
|
try
|
|
{
|
|
window.Show();
|
|
|
|
var parentContainer = (Control)menu.ContainerFromItem(parent)!;
|
|
var parentMenuItem = (INavMenuItem)parentContainer;
|
|
parentMenuItem.Open();
|
|
Dispatcher.UIThread.RunJobs();
|
|
|
|
var parentHeader = GetItemHeader(parentContainer);
|
|
var childContainer = (Control)((ItemsControl)parentContainer).ContainerFromItem(child)!;
|
|
var childHeader = GetItemHeader(childContainer);
|
|
|
|
var gap = GetTop(childHeader, menu) - GetBottom(parentHeader, menu);
|
|
|
|
gap.ShouldBe(4, 0.5,
|
|
"Ant Design does not add an extra top margin to the inline child items container.");
|
|
}
|
|
finally
|
|
{
|
|
window.Close();
|
|
}
|
|
}
|
|
|
|
[Fact]
|
|
public void Inline_Submenu_Child_Header_Gap_Collapses_AntDesign_Block_Margins()
|
|
{
|
|
var menu = new AtomUI.Desktop.Controls.NavMenu
|
|
{
|
|
Mode = NavMenuMode.Inline,
|
|
IsMotionEnabled = false,
|
|
Width = 240
|
|
};
|
|
var firstChild = new NavMenuNode
|
|
{
|
|
Header = "First child",
|
|
ItemKey = "first-child"
|
|
};
|
|
var secondChild = new NavMenuNode
|
|
{
|
|
Header = "Second child",
|
|
ItemKey = "second-child"
|
|
};
|
|
var parent = new NavMenuNode
|
|
{
|
|
Header = "Parent",
|
|
ItemKey = "parent"
|
|
};
|
|
parent.Children.Add(firstChild);
|
|
parent.Children.Add(secondChild);
|
|
menu.Items.Add(parent);
|
|
|
|
var window = new Avalonia.Controls.Window
|
|
{
|
|
Width = 320,
|
|
Height = 320,
|
|
Content = menu
|
|
};
|
|
|
|
try
|
|
{
|
|
window.Show();
|
|
|
|
var parentContainer = (Control)menu.ContainerFromItem(parent)!;
|
|
var parentMenuItem = (INavMenuItem)parentContainer;
|
|
parentMenuItem.Open();
|
|
Dispatcher.UIThread.RunJobs();
|
|
|
|
var firstChildContainer = (Control)((ItemsControl)parentContainer).ContainerFromItem(firstChild)!;
|
|
var secondChildContainer = (Control)((ItemsControl)parentContainer).ContainerFromItem(secondChild)!;
|
|
var firstChildHeader = GetItemHeader(firstChildContainer);
|
|
var secondChildHeader = GetItemHeader(secondChildContainer);
|
|
|
|
var gap = GetTop(secondChildHeader, menu) - GetBottom(firstChildHeader, menu);
|
|
|
|
gap.ShouldBe(4, 0.5,
|
|
"Inline submenu children share Ant Design's vertical menu item margin semantics.");
|
|
}
|
|
finally
|
|
{
|
|
window.Close();
|
|
}
|
|
}
|
|
|
|
[Theory]
|
|
[InlineData(true, 4d)]
|
|
[InlineData(false, 0d)]
|
|
public void Inline_Submenu_Background_Gap_Follows_Item_Background_Mode(
|
|
bool isItemBackgroundEnabled,
|
|
double expectedGap)
|
|
{
|
|
var menu = new AtomUI.Desktop.Controls.NavMenu
|
|
{
|
|
Mode = NavMenuMode.Inline,
|
|
IsMotionEnabled = false,
|
|
IsItemBackgroundEnabled = isItemBackgroundEnabled,
|
|
Width = 240
|
|
};
|
|
var firstChild = new NavMenuNode
|
|
{
|
|
Header = "First child",
|
|
ItemKey = "first-child"
|
|
};
|
|
var secondChild = new NavMenuNode
|
|
{
|
|
Header = "Second child",
|
|
ItemKey = "second-child"
|
|
};
|
|
var parent = new NavMenuNode
|
|
{
|
|
Header = "Parent",
|
|
ItemKey = "parent"
|
|
};
|
|
var nextRoot = new NavMenuNode
|
|
{
|
|
Header = "Next root",
|
|
ItemKey = "next-root"
|
|
};
|
|
parent.Children.Add(firstChild);
|
|
parent.Children.Add(secondChild);
|
|
menu.Items.Add(parent);
|
|
menu.Items.Add(nextRoot);
|
|
|
|
var window = new Avalonia.Controls.Window
|
|
{
|
|
Width = 320,
|
|
Height = 360,
|
|
Content = menu
|
|
};
|
|
|
|
try
|
|
{
|
|
window.Show();
|
|
|
|
var parentContainer = (Control)menu.ContainerFromItem(parent)!;
|
|
var parentMenuItem = (INavMenuItem)parentContainer;
|
|
parentMenuItem.Open();
|
|
menu.SelectedItem = secondChild;
|
|
Dispatcher.UIThread.RunJobs();
|
|
|
|
var childItemsFrame = FindChildItemsFrame(parentContainer);
|
|
var nextRootContainer = (Control)menu.ContainerFromItem(nextRoot)!;
|
|
var nextRootHeader = GetItemHeader(nextRootContainer);
|
|
|
|
var gap = GetTop(nextRootHeader, menu) - GetBottom(childItemsFrame, menu);
|
|
|
|
gap.ShouldBe(expectedGap, 0.5,
|
|
"The extra inline submenu block gap is only needed when the submenu background block is visible.");
|
|
}
|
|
finally
|
|
{
|
|
window.Close();
|
|
}
|
|
}
|
|
|
|
[Theory]
|
|
[InlineData(NavMenuMode.Vertical)]
|
|
[InlineData(NavMenuMode.Horizontal)]
|
|
public void Popup_Item_Inset_Uses_AntDesign_Item_Margins(NavMenuMode mode)
|
|
{
|
|
var menu = new AtomUI.Desktop.Controls.NavMenu
|
|
{
|
|
Mode = mode,
|
|
IsMotionEnabled = false,
|
|
Width = 240
|
|
};
|
|
var firstChild = new NavMenuNode
|
|
{
|
|
Header = "Item 1",
|
|
ItemKey = "item-1"
|
|
};
|
|
var secondChild = new NavMenuNode
|
|
{
|
|
Header = "Item 2",
|
|
ItemKey = "item-2"
|
|
};
|
|
var parent = new NavMenuNode
|
|
{
|
|
Header = "Parent",
|
|
ItemKey = "parent"
|
|
};
|
|
parent.Children.Add(firstChild);
|
|
parent.Children.Add(secondChild);
|
|
menu.Items.Add(parent);
|
|
|
|
var window = new Avalonia.Controls.Window
|
|
{
|
|
Width = 640,
|
|
Height = 480,
|
|
Content = CreatePopupOverlayHost(menu)
|
|
};
|
|
|
|
try
|
|
{
|
|
window.Show();
|
|
Dispatcher.UIThread.RunJobs();
|
|
|
|
var parentContainer = (Control)menu.ContainerFromItem(parent)!;
|
|
var parentMenuItem = (INavMenuItem)parentContainer;
|
|
parentMenuItem.Open();
|
|
Dispatcher.UIThread.RunJobs();
|
|
|
|
var popupFrame = FindPopupFrame(window);
|
|
var firstChildContainer = (Control)((ItemsControl)parentContainer).ContainerFromItem(firstChild)!;
|
|
var secondChildContainer = (Control)((ItemsControl)parentContainer).ContainerFromItem(secondChild)!;
|
|
var firstChildHeader = GetItemHeader(firstChildContainer);
|
|
var secondChildHeader = GetItemHeader(secondChildContainer);
|
|
|
|
var leftInset = GetLeft(firstChildHeader, popupFrame);
|
|
var rightInset = popupFrame.Bounds.Width - GetRight(firstChildHeader, popupFrame);
|
|
var topInset = GetTop(firstChildHeader, popupFrame);
|
|
var bottomInset = popupFrame.Bounds.Height - GetBottom(secondChildHeader, popupFrame);
|
|
|
|
leftInset.ShouldBe(4, 0.5,
|
|
"Ant Design popup submenus use the item's marginInline as the only horizontal inset.");
|
|
rightInset.ShouldBe(4, 0.5,
|
|
"Ant Design popup submenus use the item's marginInline as the only horizontal inset.");
|
|
topInset.ShouldBe(4, 0.5,
|
|
"Ant Design popup submenus expose one marginXXS before the first item.");
|
|
bottomInset.ShouldBe(4, 0.5,
|
|
"Ant Design popup submenus expose one marginXXS after the last item.");
|
|
}
|
|
finally
|
|
{
|
|
window.Close();
|
|
}
|
|
}
|
|
|
|
private static BaseNavMenuItemHeader GetItemHeader(Control container)
|
|
{
|
|
var field = container.GetType().GetField(
|
|
"_itemHeader",
|
|
BindingFlags.Instance | BindingFlags.NonPublic);
|
|
field.ShouldNotBeNull();
|
|
return (BaseNavMenuItemHeader)field.GetValue(container)!;
|
|
}
|
|
|
|
private static double GetTop(Control control, Visual relativeTo)
|
|
{
|
|
var point = control.TranslatePoint(default, relativeTo);
|
|
point.ShouldNotBeNull();
|
|
return point.Value.Y;
|
|
}
|
|
|
|
private static double GetLeft(Control control, Visual relativeTo)
|
|
{
|
|
var point = control.TranslatePoint(default, relativeTo);
|
|
point.ShouldNotBeNull();
|
|
return point.Value.X;
|
|
}
|
|
|
|
private static double GetRight(Control control, Visual relativeTo)
|
|
{
|
|
return GetLeft(control, relativeTo) + control.Bounds.Width;
|
|
}
|
|
|
|
private static double GetBottom(Control control, Visual relativeTo)
|
|
{
|
|
return GetTop(control, relativeTo) + control.Bounds.Height;
|
|
}
|
|
|
|
private static Border FindPopupFrame(Visual root)
|
|
{
|
|
return root.GetVisualDescendants()
|
|
.OfType<Border>()
|
|
.Single(border => border.Name == "PART_PopupFrame");
|
|
}
|
|
|
|
private static Border FindChildItemsFrame(Visual root)
|
|
{
|
|
return root.GetVisualDescendants()
|
|
.OfType<Border>()
|
|
.Single(border => border.Name == "PART_ChildItemsFrame");
|
|
}
|
|
|
|
private static VisualLayerManager CreatePopupOverlayHost(Control content)
|
|
{
|
|
var visualLayerManager = new VisualLayerManager
|
|
{
|
|
Child = content
|
|
};
|
|
|
|
EnablePopupOverlayLayer(visualLayerManager);
|
|
return visualLayerManager;
|
|
}
|
|
|
|
private static void EnablePopupOverlayLayer(VisualLayerManager visualLayerManager)
|
|
{
|
|
var property = typeof(VisualLayerManager).GetProperty(
|
|
"EnablePopupOverlayLayer",
|
|
BindingFlags.Instance | BindingFlags.NonPublic);
|
|
|
|
property.ShouldNotBeNull();
|
|
property.SetValue(visualLayerManager, true);
|
|
}
|
|
}
|