mirror of
https://github.com/AtomUI/AtomUI.git
synced 2026-10-18 22:46:38 +08:00
1223 lines
37 KiB
C#
1223 lines
37 KiB
C#
using System.Reflection;
|
|
using AtomUI.Controls.Primitives;
|
|
using AtomUI.MotionScene;
|
|
using Avalonia;
|
|
using Avalonia.Controls;
|
|
using Avalonia.Headless;
|
|
using Avalonia.Input;
|
|
using Avalonia.Media;
|
|
using Avalonia.Threading;
|
|
using Shouldly;
|
|
using Xunit;
|
|
|
|
namespace AtomUI.Desktop.Controls.Tests.NavMenu;
|
|
|
|
public class NavMenuSelectionTests
|
|
{
|
|
static NavMenuSelectionTests()
|
|
{
|
|
AvaloniaTestApp.EnsureInitialized();
|
|
}
|
|
|
|
[Fact]
|
|
public void NavMenu_Ignores_Stale_Async_SelectedItem_Replay()
|
|
{
|
|
var menu = new AtomUI.Desktop.Controls.NavMenu
|
|
{
|
|
Mode = NavMenuMode.Inline,
|
|
IsMotionEnabled = false
|
|
};
|
|
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 selectedNodes = new List<INavMenuNode>();
|
|
menu.NavMenuNodeSelected += (_, args) => selectedNodes.Add(args.NavMenuNode);
|
|
|
|
var window = new Avalonia.Controls.Window
|
|
{
|
|
Width = 320,
|
|
Height = 240,
|
|
Content = menu
|
|
};
|
|
|
|
try
|
|
{
|
|
window.Show();
|
|
Dispatcher.UIThread.RunJobs();
|
|
|
|
menu.SelectedItem = first;
|
|
menu.SelectedItem = second;
|
|
Dispatcher.UIThread.RunJobs();
|
|
|
|
selectedNodes.ShouldBe([second]);
|
|
menu.SelectedItem.ShouldBeSameAs(second);
|
|
}
|
|
finally
|
|
{
|
|
window.Close();
|
|
}
|
|
}
|
|
|
|
[Theory]
|
|
[InlineData(NavMenuMode.Horizontal)]
|
|
[InlineData(NavMenuMode.Vertical)]
|
|
[InlineData(NavMenuMode.Inline)]
|
|
public void DefaultSelectedPath_Selects_Nested_Item_Without_Waiting_For_Timer(NavMenuMode mode)
|
|
{
|
|
var leaf = new NavMenuNode
|
|
{
|
|
Header = "Leaf",
|
|
ItemKey = "leaf"
|
|
};
|
|
var child = new NavMenuNode
|
|
{
|
|
Header = "Child",
|
|
ItemKey = "child"
|
|
};
|
|
child.Children.Add(leaf);
|
|
var parent = new NavMenuNode
|
|
{
|
|
Header = "Parent",
|
|
ItemKey = "parent"
|
|
};
|
|
parent.Children.Add(child);
|
|
|
|
var menu = new AtomUI.Desktop.Controls.NavMenu
|
|
{
|
|
Mode = mode,
|
|
IsMotionEnabled = false,
|
|
DefaultSelectedPath = new TreeNodePath("parent/child/leaf")
|
|
};
|
|
menu.Items.Add(parent);
|
|
|
|
var selectedNodes = new List<INavMenuNode>();
|
|
menu.NavMenuNodeSelected += (_, args) => selectedNodes.Add(args.NavMenuNode);
|
|
|
|
var window = new Avalonia.Controls.Window
|
|
{
|
|
Width = 320,
|
|
Height = 240,
|
|
Content = mode == NavMenuMode.Inline ? menu : CreatePopupOverlayHost(menu)
|
|
};
|
|
|
|
try
|
|
{
|
|
window.Show();
|
|
RunDispatcherJobsUntil(() => ReferenceEquals(menu.SelectedItem, leaf));
|
|
|
|
menu.SelectedItem.ShouldBeSameAs(leaf);
|
|
selectedNodes.ShouldBe([leaf]);
|
|
}
|
|
finally
|
|
{
|
|
window.Close();
|
|
}
|
|
}
|
|
|
|
[Fact]
|
|
public void DefaultSelectedPath_Traverses_Groups_Without_Consuming_Path_Segments()
|
|
{
|
|
var leaf = new NavMenuNode
|
|
{
|
|
Header = "Leaf",
|
|
ItemKey = "leaf"
|
|
};
|
|
var childGroup = new NavMenuGroup { Header = "Child group" };
|
|
childGroup.Entries.Add(leaf);
|
|
var parent = new NavMenuNode
|
|
{
|
|
Header = "Parent",
|
|
ItemKey = "parent"
|
|
};
|
|
parent.Entries.Add(childGroup);
|
|
var rootGroup = new NavMenuGroup { Header = "Root group" };
|
|
rootGroup.Entries.Add(parent);
|
|
|
|
var menu = new AtomUI.Desktop.Controls.NavMenu
|
|
{
|
|
Mode = NavMenuMode.Inline,
|
|
IsMotionEnabled = false,
|
|
DefaultSelectedPath = new TreeNodePath("parent/leaf")
|
|
};
|
|
menu.Items.Add(rootGroup);
|
|
|
|
var window = new Avalonia.Controls.Window
|
|
{
|
|
Width = 320,
|
|
Height = 240,
|
|
Content = menu
|
|
};
|
|
|
|
try
|
|
{
|
|
window.Show();
|
|
RunDispatcherJobsUntil(() => ReferenceEquals(menu.SelectedItem, leaf));
|
|
|
|
var rootGroupContainer = menu.ContainerFromItem(rootGroup).ShouldBeOfType<NavMenuGroupItem>();
|
|
var parentContainer = rootGroupContainer.ContainerFromItem(parent).ShouldBeOfType<NavMenuItem>();
|
|
var childGroupContainer = parentContainer.ContainerFromItem(childGroup).ShouldBeOfType<NavMenuGroupItem>();
|
|
var leafContainer = childGroupContainer.ContainerFromItem(leaf).ShouldBeOfType<NavMenuItem>();
|
|
menu.SelectedItem.ShouldBeSameAs(leaf);
|
|
parentContainer.IsInSelectedPath.ShouldBeTrue();
|
|
leafContainer.IsSelected.ShouldBeTrue();
|
|
}
|
|
finally
|
|
{
|
|
window.Close();
|
|
}
|
|
}
|
|
|
|
[Theory]
|
|
[InlineData(NavMenuMode.Horizontal)]
|
|
[InlineData(NavMenuMode.Vertical)]
|
|
[InlineData(NavMenuMode.Inline)]
|
|
public void DefaultOpenPaths_Opens_Nested_Submenu_Without_Waiting_For_Timer(NavMenuMode mode)
|
|
{
|
|
var leaf = new NavMenuNode
|
|
{
|
|
Header = "Leaf",
|
|
ItemKey = "leaf"
|
|
};
|
|
var child = new NavMenuNode
|
|
{
|
|
Header = "Child",
|
|
ItemKey = "child"
|
|
};
|
|
child.Children.Add(leaf);
|
|
var parent = new NavMenuNode
|
|
{
|
|
Header = "Parent",
|
|
ItemKey = "parent"
|
|
};
|
|
parent.Children.Add(child);
|
|
|
|
var menu = new AtomUI.Desktop.Controls.NavMenu
|
|
{
|
|
Mode = mode,
|
|
IsMotionEnabled = false,
|
|
DefaultOpenPaths =
|
|
[
|
|
new TreeNodePath("parent/child")
|
|
]
|
|
};
|
|
menu.Items.Add(parent);
|
|
|
|
var window = new Avalonia.Controls.Window
|
|
{
|
|
Width = 320,
|
|
Height = 240,
|
|
Content = mode == NavMenuMode.Inline ? menu : CreatePopupOverlayHost(menu)
|
|
};
|
|
|
|
try
|
|
{
|
|
window.Show();
|
|
RunDispatcherJobsUntil(() =>
|
|
{
|
|
var parentContainer = menu.ContainerFromItem(parent) as NavMenuItem;
|
|
var childContainer = parentContainer?.ContainerFromItem(child) as NavMenuItem;
|
|
return parentContainer?.IsSubMenuOpen == true &&
|
|
childContainer?.IsSubMenuOpen == true;
|
|
});
|
|
|
|
var realizedParent = menu.ContainerFromItem(parent) as NavMenuItem;
|
|
var realizedChild = realizedParent?.ContainerFromItem(child) as NavMenuItem;
|
|
realizedParent.ShouldNotBeNull();
|
|
realizedChild.ShouldNotBeNull();
|
|
realizedParent.IsSubMenuOpen.ShouldBeTrue();
|
|
realizedChild.IsSubMenuOpen.ShouldBeTrue();
|
|
}
|
|
finally
|
|
{
|
|
window.Close();
|
|
}
|
|
}
|
|
|
|
[Theory]
|
|
[InlineData(NavMenuMode.Vertical)]
|
|
[InlineData(NavMenuMode.Horizontal)]
|
|
[InlineData(NavMenuMode.Inline)]
|
|
public void NavMenu_Clears_Selected_Container_When_SelectedItem_Is_Set_To_Null(NavMenuMode mode)
|
|
{
|
|
var menu = new AtomUI.Desktop.Controls.NavMenu
|
|
{
|
|
Mode = mode,
|
|
IsMotionEnabled = false
|
|
};
|
|
var first = new NavMenuNode
|
|
{
|
|
Header = "First",
|
|
ItemKey = "first"
|
|
};
|
|
menu.Items.Add(first);
|
|
|
|
var selectedNodes = new List<INavMenuNode>();
|
|
menu.NavMenuNodeSelected += (_, args) => selectedNodes.Add(args.NavMenuNode);
|
|
|
|
var window = new Avalonia.Controls.Window
|
|
{
|
|
Width = 320,
|
|
Height = 240,
|
|
Content = menu
|
|
};
|
|
|
|
try
|
|
{
|
|
window.Show();
|
|
Dispatcher.UIThread.RunJobs();
|
|
|
|
menu.SelectedItem = first;
|
|
Dispatcher.UIThread.RunJobs();
|
|
|
|
var firstContainer = menu.ContainerFromItem(first) as Control;
|
|
firstContainer.ShouldNotBeNull();
|
|
IsSelected(firstContainer).ShouldBeTrue();
|
|
|
|
menu.SelectedItem = null;
|
|
Dispatcher.UIThread.RunJobs();
|
|
|
|
menu.SelectedItem.ShouldBeNull();
|
|
IsSelected(firstContainer).ShouldBeFalse();
|
|
|
|
selectedNodes.Clear();
|
|
menu.SelectedItem = first;
|
|
Dispatcher.UIThread.RunJobs();
|
|
|
|
IsSelected(firstContainer).ShouldBeTrue();
|
|
selectedNodes.ShouldBe([first]);
|
|
}
|
|
finally
|
|
{
|
|
window.Close();
|
|
}
|
|
}
|
|
|
|
[Fact]
|
|
public void NavMenu_Selects_New_Item_After_Replacing_The_Selected_Item_Tree()
|
|
{
|
|
var first = new NavMenuNode
|
|
{
|
|
Header = "First",
|
|
ItemKey = "first"
|
|
};
|
|
var replacement = new NavMenuNode
|
|
{
|
|
Header = "Replacement",
|
|
ItemKey = "replacement"
|
|
};
|
|
var menu = new AtomUI.Desktop.Controls.NavMenu
|
|
{
|
|
Mode = NavMenuMode.Inline,
|
|
IsMotionEnabled = false
|
|
};
|
|
menu.Items.Add(first);
|
|
|
|
var window = new Avalonia.Controls.Window
|
|
{
|
|
Width = 320,
|
|
Height = 240,
|
|
Content = menu
|
|
};
|
|
|
|
try
|
|
{
|
|
window.Show();
|
|
Dispatcher.UIThread.RunJobs();
|
|
|
|
var firstContainer = menu.ContainerFromItem(first).ShouldBeOfType<NavMenuItem>();
|
|
menu.InteractionHandler.ShouldNotBeNull();
|
|
menu.SelectNavMenuItem(firstContainer);
|
|
|
|
menu.Items.Clear();
|
|
menu.Items.Add(replacement);
|
|
Dispatcher.UIThread.RunJobs();
|
|
|
|
var replacementContainer = menu.ContainerFromItem(replacement).ShouldBeOfType<NavMenuItem>();
|
|
var replacementHeader = GetItemHeader(replacementContainer);
|
|
var replacementPoint = GetCenterPoint(replacementHeader, window);
|
|
Should.NotThrow(() =>
|
|
{
|
|
window.MouseDown(replacementPoint, MouseButton.Left);
|
|
Dispatcher.UIThread.RunJobs();
|
|
window.MouseUp(replacementPoint, MouseButton.Left);
|
|
Dispatcher.UIThread.RunJobs();
|
|
});
|
|
|
|
replacementContainer.IsSelected.ShouldBeTrue();
|
|
menu.SelectedItem.ShouldBeSameAs(replacement);
|
|
}
|
|
finally
|
|
{
|
|
window.Close();
|
|
}
|
|
}
|
|
|
|
[Fact]
|
|
public void NavMenu_Detach_Then_Programmatic_Sibling_Selection_Clears_Previous_Selection()
|
|
{
|
|
var first = new NavMenuNode
|
|
{
|
|
Header = "First",
|
|
ItemKey = "first"
|
|
};
|
|
var second = new NavMenuNode
|
|
{
|
|
Header = "Second",
|
|
ItemKey = "second"
|
|
};
|
|
var menu = new AtomUI.Desktop.Controls.NavMenu
|
|
{
|
|
Mode = NavMenuMode.Inline,
|
|
IsMotionEnabled = false
|
|
};
|
|
menu.Items.Add(first);
|
|
menu.Items.Add(second);
|
|
|
|
var window = new Avalonia.Controls.Window
|
|
{
|
|
Width = 320,
|
|
Height = 240,
|
|
Content = menu
|
|
};
|
|
|
|
try
|
|
{
|
|
window.Show();
|
|
Dispatcher.UIThread.RunJobs();
|
|
|
|
menu.SelectedItem = first;
|
|
Dispatcher.UIThread.RunJobs();
|
|
|
|
var firstContainer = menu.ContainerFromItem(first).ShouldBeOfType<NavMenuItem>();
|
|
firstContainer.IsSelected.ShouldBeTrue();
|
|
|
|
window.Content = null;
|
|
Dispatcher.UIThread.RunJobs();
|
|
|
|
menu.SelectedItem = second;
|
|
window.Content = menu;
|
|
Dispatcher.UIThread.RunJobs();
|
|
|
|
var secondContainer = menu.ContainerFromItem(second).ShouldBeOfType<NavMenuItem>();
|
|
firstContainer.IsSelected.ShouldBeFalse(
|
|
"visual detach must not discard the identity needed to clear the applied selection.");
|
|
secondContainer.IsSelected.ShouldBeTrue();
|
|
}
|
|
finally
|
|
{
|
|
window.Close();
|
|
}
|
|
}
|
|
|
|
[Fact]
|
|
public void Inline_Submenu_Child_Press_Does_Not_Apply_Active_Background_To_Parent_Header()
|
|
{
|
|
var menu = new AtomUI.Desktop.Controls.NavMenu
|
|
{
|
|
Mode = NavMenuMode.Inline,
|
|
IsMotionEnabled = false
|
|
};
|
|
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 = 240,
|
|
Content = menu
|
|
};
|
|
|
|
try
|
|
{
|
|
window.Show();
|
|
Dispatcher.UIThread.RunJobs();
|
|
|
|
var parentContainer = (Control)menu.ContainerFromItem(parent)!;
|
|
var parentMenuItem = (INavMenuItem)parentContainer;
|
|
parentMenuItem.Open();
|
|
Dispatcher.UIThread.RunJobs();
|
|
|
|
var childContainer = (Control)((ItemsControl)parentContainer).ContainerFromItem(child)!;
|
|
var parentHeader = GetItemHeader(parentContainer);
|
|
var childHeader = GetItemHeader(childContainer);
|
|
|
|
MouseDown(childHeader, window);
|
|
Dispatcher.UIThread.RunJobs();
|
|
|
|
GetSolidBrushColor(parentHeader.Background).ShouldBe(
|
|
Colors.Transparent,
|
|
"pressing a child menu item must not make its parent header look active");
|
|
}
|
|
finally
|
|
{
|
|
window.Close();
|
|
}
|
|
}
|
|
|
|
[Fact]
|
|
public void Inline_Selection_Marks_All_Ancestor_Submenus_As_Selected_Path()
|
|
{
|
|
var menu = new AtomUI.Desktop.Controls.NavMenu
|
|
{
|
|
Mode = NavMenuMode.Inline,
|
|
IsMotionEnabled = false
|
|
};
|
|
var leaf = new NavMenuNode
|
|
{
|
|
Header = "Option 1",
|
|
ItemKey = "option-1"
|
|
};
|
|
var secondLevel = new NavMenuNode
|
|
{
|
|
Header = "Item 1",
|
|
ItemKey = "item-1"
|
|
};
|
|
secondLevel.Children.Add(leaf);
|
|
var firstLevel = new NavMenuNode
|
|
{
|
|
Header = "Navigation Three - Submenu",
|
|
ItemKey = "navigation-three"
|
|
};
|
|
firstLevel.Children.Add(secondLevel);
|
|
menu.Items.Add(firstLevel);
|
|
|
|
var window = new Avalonia.Controls.Window
|
|
{
|
|
Width = 320,
|
|
Height = 360,
|
|
Content = menu
|
|
};
|
|
|
|
try
|
|
{
|
|
window.Show();
|
|
Dispatcher.UIThread.RunJobs();
|
|
|
|
var firstLevelContainer = (NavMenuItem)menu.ContainerFromItem(firstLevel)!;
|
|
firstLevelContainer.Open();
|
|
Dispatcher.UIThread.RunJobs();
|
|
|
|
var secondLevelContainer = (NavMenuItem)firstLevelContainer.ContainerFromItem(secondLevel)!;
|
|
secondLevelContainer.Open();
|
|
Dispatcher.UIThread.RunJobs();
|
|
|
|
var leafContainer = (NavMenuItem)secondLevelContainer.ContainerFromItem(leaf)!;
|
|
menu.InteractionHandler.ShouldNotBeNull();
|
|
menu.SelectNavMenuItem(leafContainer);
|
|
Dispatcher.UIThread.RunJobs();
|
|
|
|
firstLevelContainer.IsInSelectedPath.ShouldBeTrue(
|
|
"Ant Design colors every ancestor submenu title in the selected path.");
|
|
secondLevelContainer.IsInSelectedPath.ShouldBeTrue(
|
|
"Every inline ancestor must expose selected-path state to its header.");
|
|
leafContainer.IsSelected.ShouldBeTrue();
|
|
|
|
var firstLevelHeader = GetItemHeader(firstLevelContainer);
|
|
var secondLevelHeader = GetItemHeader(secondLevelContainer);
|
|
firstLevelHeader.IsInSelectedPath.ShouldBeTrue(
|
|
"The selected-path state must reach the first ancestor header so its icon, text and arrow use the selected color.");
|
|
secondLevelHeader.IsInSelectedPath.ShouldBeTrue(
|
|
"Nested ancestor headers must receive selected-path state consistently.");
|
|
}
|
|
finally
|
|
{
|
|
window.Close();
|
|
}
|
|
}
|
|
|
|
[Fact]
|
|
public void Root_Group_Leaf_Selection_Uses_The_Root_Semantic_Selection_Owner()
|
|
{
|
|
var leaf = new NavMenuNode
|
|
{
|
|
Header = "Leaf",
|
|
ItemKey = "leaf"
|
|
};
|
|
var group = new NavMenuGroup { Header = "Section" };
|
|
group.Entries.Add(leaf);
|
|
var menu = new AtomUI.Desktop.Controls.NavMenu
|
|
{
|
|
Mode = NavMenuMode.Inline,
|
|
IsMotionEnabled = false
|
|
};
|
|
menu.Items.Add(group);
|
|
|
|
var window = new Avalonia.Controls.Window
|
|
{
|
|
Width = 320,
|
|
Height = 240,
|
|
Content = menu
|
|
};
|
|
|
|
try
|
|
{
|
|
window.Show();
|
|
Dispatcher.UIThread.RunJobs();
|
|
|
|
var groupContainer = menu.ContainerFromItem(group).ShouldBeOfType<NavMenuGroupItem>();
|
|
var leafContainer = groupContainer.ContainerFromItem(leaf).ShouldBeOfType<NavMenuItem>();
|
|
menu.InteractionHandler.ShouldNotBeNull();
|
|
menu.SelectNavMenuItem(leafContainer);
|
|
|
|
leafContainer.IsSelected.ShouldBeTrue();
|
|
menu.SelectedItem.ShouldBeSameAs(leaf);
|
|
}
|
|
finally
|
|
{
|
|
window.Close();
|
|
}
|
|
}
|
|
|
|
[Fact]
|
|
public void Inline_Selection_Through_A_Group_Uses_The_Closest_Node_Selected_Path()
|
|
{
|
|
var leaf = new NavMenuNode
|
|
{
|
|
Header = "Leaf",
|
|
ItemKey = "leaf"
|
|
};
|
|
var group = new NavMenuGroup { Header = "Section" };
|
|
group.Entries.Add(leaf);
|
|
var parent = new NavMenuNode
|
|
{
|
|
Header = "Parent",
|
|
ItemKey = "parent"
|
|
};
|
|
parent.Entries.Add(group);
|
|
var menu = new AtomUI.Desktop.Controls.NavMenu
|
|
{
|
|
Mode = NavMenuMode.Inline,
|
|
IsMotionEnabled = false
|
|
};
|
|
menu.Items.Add(parent);
|
|
|
|
var window = new Avalonia.Controls.Window
|
|
{
|
|
Width = 320,
|
|
Height = 240,
|
|
Content = menu
|
|
};
|
|
|
|
try
|
|
{
|
|
window.Show();
|
|
Dispatcher.UIThread.RunJobs();
|
|
|
|
var parentContainer = menu.ContainerFromItem(parent).ShouldBeOfType<NavMenuItem>();
|
|
parentContainer.Open();
|
|
Dispatcher.UIThread.RunJobs();
|
|
var groupContainer = parentContainer.ContainerFromItem(group).ShouldBeOfType<NavMenuGroupItem>();
|
|
var leafContainer = groupContainer.ContainerFromItem(leaf).ShouldBeOfType<NavMenuItem>();
|
|
menu.InteractionHandler.ShouldNotBeNull();
|
|
menu.SelectNavMenuItem(leafContainer);
|
|
|
|
parentContainer.IsInSelectedPath.ShouldBeTrue();
|
|
leafContainer.IsSelected.ShouldBeTrue();
|
|
menu.SelectedItem.ShouldBeSameAs(leaf);
|
|
}
|
|
finally
|
|
{
|
|
window.Close();
|
|
}
|
|
}
|
|
|
|
[Fact]
|
|
public void Inline_Selected_Path_Submenu_Header_Keeps_Selected_Foreground_When_Hovered()
|
|
{
|
|
var menu = new AtomUI.Desktop.Controls.NavMenu
|
|
{
|
|
Mode = NavMenuMode.Inline,
|
|
IsMotionEnabled = false
|
|
};
|
|
var leaf = new NavMenuNode
|
|
{
|
|
Header = "Option 1",
|
|
ItemKey = "option-1"
|
|
};
|
|
var parent = new NavMenuNode
|
|
{
|
|
Header = "Navigation Three - Submenu",
|
|
ItemKey = "navigation-three"
|
|
};
|
|
parent.Children.Add(leaf);
|
|
menu.Items.Add(parent);
|
|
|
|
var window = new Avalonia.Controls.Window
|
|
{
|
|
Width = 320,
|
|
Height = 240,
|
|
Content = menu
|
|
};
|
|
|
|
try
|
|
{
|
|
window.Show();
|
|
Dispatcher.UIThread.RunJobs();
|
|
|
|
var parentContainer = (NavMenuItem)menu.ContainerFromItem(parent)!;
|
|
parentContainer.Open();
|
|
Dispatcher.UIThread.RunJobs();
|
|
|
|
var leafContainer = (NavMenuItem)parentContainer.ContainerFromItem(leaf)!;
|
|
menu.InteractionHandler.ShouldNotBeNull();
|
|
menu.SelectNavMenuItem(leafContainer);
|
|
Dispatcher.UIThread.RunJobs();
|
|
|
|
var parentHeader = GetItemHeader(parentContainer);
|
|
var selectedForegroundBefore = GetSolidBrushColor(parentHeader.Foreground);
|
|
selectedForegroundBefore.ShouldNotBeNull();
|
|
parentHeader.IsInSelectedPath.ShouldBeTrue();
|
|
|
|
MouseMove(parentHeader, window);
|
|
Dispatcher.UIThread.RunJobs();
|
|
|
|
GetSolidBrushColor(parentHeader.Foreground).ShouldBe(
|
|
selectedForegroundBefore,
|
|
"hover must not override the selected-path submenu title foreground; Ant Design keeps selected ancestor submenu titles in the selected color.");
|
|
}
|
|
finally
|
|
{
|
|
window.Close();
|
|
}
|
|
}
|
|
|
|
[Fact]
|
|
public void Inline_Item_Background_Control_Propagates_To_Header_And_Submenu_Frame()
|
|
{
|
|
var menu = new AtomUI.Desktop.Controls.NavMenu
|
|
{
|
|
Mode = NavMenuMode.Inline,
|
|
IsMotionEnabled = false,
|
|
IsItemBackgroundEnabled = false
|
|
};
|
|
var grandchild = new NavMenuNode
|
|
{
|
|
Header = "Grandchild",
|
|
ItemKey = "grandchild"
|
|
};
|
|
var child = new NavMenuNode
|
|
{
|
|
Header = "Child",
|
|
ItemKey = "child"
|
|
};
|
|
child.Children.Add(grandchild);
|
|
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 = 240,
|
|
Content = menu
|
|
};
|
|
|
|
try
|
|
{
|
|
window.Show();
|
|
Dispatcher.UIThread.RunJobs();
|
|
|
|
var parentContainer = (Control)menu.ContainerFromItem(parent)!;
|
|
var parentMenuItem = (INavMenuItem)parentContainer;
|
|
parentMenuItem.Open();
|
|
Dispatcher.UIThread.RunJobs();
|
|
|
|
var childContainer = (NavMenuItem)((ItemsControl)parentContainer).ContainerFromItem(child)!;
|
|
var childMenuItem = (INavMenuItem)childContainer;
|
|
childMenuItem.Open();
|
|
Dispatcher.UIThread.RunJobs();
|
|
|
|
var grandchildContainer = (NavMenuItem)childContainer.ContainerFromItem(grandchild)!;
|
|
var parentHeader = GetItemHeader(parentContainer);
|
|
var childHeader = GetItemHeader(childContainer);
|
|
var grandchildHeader = GetItemHeader(grandchildContainer);
|
|
var childFrame = GetChildItemsFrame(parentContainer);
|
|
var grandchildFrame = GetChildItemsFrame(childContainer);
|
|
|
|
parentHeader.IsItemBackgroundEnabled.ShouldBeFalse();
|
|
childHeader.IsItemBackgroundEnabled.ShouldBeFalse(
|
|
"the menu-level background control must be inherited by nested inline menu items.");
|
|
grandchildHeader.IsItemBackgroundEnabled.ShouldBeFalse(
|
|
"every realized descendant header must receive the same background control value.");
|
|
GetSolidBrushColor(parentHeader.Background).ShouldBe(Colors.Transparent);
|
|
GetSolidBrushColor(childFrame.Background).ShouldBe(Colors.Transparent);
|
|
GetSolidBrushColor(grandchildFrame.Background).ShouldBe(Colors.Transparent);
|
|
|
|
MouseMove(grandchildHeader, window);
|
|
Dispatcher.UIThread.RunJobs();
|
|
|
|
GetSolidBrushColor(grandchildHeader.Background).ShouldNotBe(
|
|
Colors.Transparent,
|
|
"NavMenuItemHeader hover visuals must stay available when NavMenuItem background is disabled.");
|
|
|
|
menu.InteractionHandler.ShouldNotBeNull();
|
|
menu.SelectNavMenuItem(grandchildContainer);
|
|
Dispatcher.UIThread.RunJobs();
|
|
|
|
GetSolidBrushColor(grandchildHeader.Background).ShouldNotBe(
|
|
Colors.Transparent,
|
|
"NavMenuItemHeader selected visuals must stay available when NavMenuItem background is disabled.");
|
|
|
|
menu.IsItemBackgroundEnabled = true;
|
|
Dispatcher.UIThread.RunJobs();
|
|
|
|
parentHeader.IsItemBackgroundEnabled.ShouldBeTrue();
|
|
childHeader.IsItemBackgroundEnabled.ShouldBeTrue();
|
|
grandchildHeader.IsItemBackgroundEnabled.ShouldBeTrue();
|
|
}
|
|
finally
|
|
{
|
|
window.Close();
|
|
}
|
|
}
|
|
|
|
[Theory]
|
|
[InlineData(NavMenuMode.Horizontal)]
|
|
[InlineData(NavMenuMode.Vertical)]
|
|
[InlineData(NavMenuMode.Inline)]
|
|
public void Selecting_Realized_Item_Does_Not_Temporarily_Disable_Motion(NavMenuMode mode)
|
|
{
|
|
var menu = new AtomUI.Desktop.Controls.NavMenu
|
|
{
|
|
Mode = mode,
|
|
IsMotionEnabled = true
|
|
};
|
|
var item = new NavMenuNode
|
|
{
|
|
Header = "Item",
|
|
ItemKey = "item"
|
|
};
|
|
menu.Items.Add(item);
|
|
|
|
var disabledDuringSelection = false;
|
|
menu.PropertyChanged += (_, args) =>
|
|
{
|
|
if (args.Property == AtomUI.Desktop.Controls.NavMenu.IsMotionEnabledProperty &&
|
|
args.NewValue is false)
|
|
{
|
|
disabledDuringSelection = true;
|
|
}
|
|
};
|
|
|
|
var window = new Avalonia.Controls.Window
|
|
{
|
|
Width = 320,
|
|
Height = 240,
|
|
Content = menu
|
|
};
|
|
|
|
try
|
|
{
|
|
window.Show();
|
|
Dispatcher.UIThread.RunJobs();
|
|
|
|
var itemContainer = (NavMenuItem)menu.ContainerFromItem(item)!;
|
|
menu.InteractionHandler.ShouldNotBeNull();
|
|
menu.SelectNavMenuItem(itemContainer);
|
|
Dispatcher.UIThread.RunJobs();
|
|
|
|
disabledDuringSelection.ShouldBeFalse(
|
|
"a user item selection is already applied by the interaction handler and must not replay the path by disabling motion");
|
|
}
|
|
finally
|
|
{
|
|
window.Close();
|
|
}
|
|
}
|
|
|
|
[Fact]
|
|
public void Inline_Submenu_Open_With_Motion_Disabled_Restores_Actor_Opacity_After_Animated_Close()
|
|
{
|
|
var menu = new AtomUI.Desktop.Controls.NavMenu
|
|
{
|
|
Mode = NavMenuMode.Inline,
|
|
IsMotionEnabled = true
|
|
};
|
|
|
|
var parent = new NavMenuNode
|
|
{
|
|
Header = "Parent",
|
|
ItemKey = "parent"
|
|
};
|
|
parent.Children.Add(new NavMenuNode
|
|
{
|
|
Header = "Child",
|
|
ItemKey = "child"
|
|
});
|
|
menu.Items.Add(parent);
|
|
|
|
var window = new Avalonia.Controls.Window
|
|
{
|
|
Width = 320,
|
|
Height = 240,
|
|
Content = menu
|
|
};
|
|
|
|
try
|
|
{
|
|
window.Show();
|
|
Dispatcher.UIThread.RunJobs();
|
|
|
|
var parentContainer = (Control)menu.ContainerFromItem(parent)!;
|
|
SetTimeSpanProperty(parentContainer, "OpenCloseMotionDuration", TimeSpan.FromMilliseconds(1));
|
|
|
|
var actor = GetChildItemsMotionActor(parentContainer);
|
|
var parentMenuItem = (INavMenuItem)parentContainer;
|
|
parentMenuItem.Open();
|
|
WaitForInlineMotionCompletion(actor);
|
|
|
|
parentMenuItem.Close();
|
|
WaitForInlineMotionCompletion(actor);
|
|
|
|
actor.IsVisible.ShouldBeFalse();
|
|
actor.Opacity.ShouldBe(0.0);
|
|
|
|
menu.IsMotionEnabled = false;
|
|
DrainDispatcher();
|
|
|
|
parentMenuItem.Open();
|
|
DrainDispatcher();
|
|
|
|
actor.IsVisible.ShouldBeTrue();
|
|
actor.Opacity.ShouldBe(1.0);
|
|
}
|
|
finally
|
|
{
|
|
window.Close();
|
|
}
|
|
}
|
|
|
|
[Fact]
|
|
public void Inline_Submenu_Open_With_Motion_Disabled_Clears_Child_Header_Transitions_After_Animated_Close()
|
|
{
|
|
var menu = new AtomUI.Desktop.Controls.NavMenu
|
|
{
|
|
Mode = NavMenuMode.Inline,
|
|
IsMotionEnabled = true
|
|
};
|
|
|
|
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 = 240,
|
|
Content = menu
|
|
};
|
|
|
|
try
|
|
{
|
|
window.Show();
|
|
Dispatcher.UIThread.RunJobs();
|
|
|
|
var parentContainer = (Control)menu.ContainerFromItem(parent)!;
|
|
SetTimeSpanProperty(parentContainer, "OpenCloseMotionDuration", TimeSpan.FromMilliseconds(1));
|
|
|
|
var parentMenuItem = (INavMenuItem)parentContainer;
|
|
parentMenuItem.Open();
|
|
DrainDispatcher();
|
|
|
|
var childContainer = (Control)((ItemsControl)parentContainer).ContainerFromItem(child)!;
|
|
var childHeader = GetItemHeader(childContainer);
|
|
|
|
childHeader.IsMotionEnabled.ShouldBeTrue();
|
|
childHeader.Transitions.ShouldNotBeNull();
|
|
|
|
parentMenuItem.Close();
|
|
DrainDispatcher();
|
|
|
|
menu.IsMotionEnabled = false;
|
|
DrainDispatcher();
|
|
|
|
parentMenuItem.Open();
|
|
DrainDispatcher();
|
|
|
|
childHeader.IsMotionEnabled.ShouldBeFalse();
|
|
childHeader.Transitions.ShouldBeNull();
|
|
}
|
|
finally
|
|
{
|
|
window.Close();
|
|
}
|
|
}
|
|
|
|
[Fact]
|
|
public void Inline_Submenu_Open_With_Motion_Disabled_Overrides_Pending_Animated_Close()
|
|
{
|
|
var menu = new AtomUI.Desktop.Controls.NavMenu
|
|
{
|
|
Mode = NavMenuMode.Inline,
|
|
IsMotionEnabled = true
|
|
};
|
|
|
|
var parent = new NavMenuNode
|
|
{
|
|
Header = "Parent",
|
|
ItemKey = "parent"
|
|
};
|
|
parent.Children.Add(new NavMenuNode
|
|
{
|
|
Header = "Child",
|
|
ItemKey = "child"
|
|
});
|
|
menu.Items.Add(parent);
|
|
|
|
var window = new Avalonia.Controls.Window
|
|
{
|
|
Width = 320,
|
|
Height = 240,
|
|
Content = menu
|
|
};
|
|
|
|
try
|
|
{
|
|
window.Show();
|
|
Dispatcher.UIThread.RunJobs();
|
|
|
|
var parentContainer = (Control)menu.ContainerFromItem(parent)!;
|
|
var parentMenuItem = (INavMenuItem)parentContainer;
|
|
|
|
SetTimeSpanProperty(parentContainer, "OpenCloseMotionDuration", TimeSpan.FromMilliseconds(1));
|
|
parentMenuItem.Open();
|
|
WaitForInlineMotionCompletion(GetChildItemsMotionActor(parentContainer));
|
|
|
|
SetTimeSpanProperty(parentContainer, "OpenCloseMotionDuration", TimeSpan.FromMilliseconds(500));
|
|
parentMenuItem.Close();
|
|
Dispatcher.UIThread.RunJobs();
|
|
Dispatcher.UIThread.RunJobs();
|
|
|
|
var actor = GetChildItemsMotionActor(parentContainer);
|
|
actor.IsVisible.ShouldBeTrue();
|
|
actor.IsAnimating(Visual.OpacityProperty).ShouldBeTrue();
|
|
|
|
menu.IsMotionEnabled = false;
|
|
Dispatcher.UIThread.RunJobs();
|
|
|
|
parentMenuItem.Open();
|
|
Dispatcher.UIThread.RunJobs();
|
|
|
|
parentMenuItem.IsSubMenuOpen.ShouldBeTrue();
|
|
|
|
actor.IsAnimating(Visual.OpacityProperty).ShouldBeFalse();
|
|
Thread.Sleep(600);
|
|
AvaloniaHeadlessPlatform.ForceRenderTimerTick();
|
|
Dispatcher.UIThread.RunJobs();
|
|
|
|
actor.IsVisible.ShouldBeTrue();
|
|
actor.Opacity.ShouldBe(1.0);
|
|
}
|
|
finally
|
|
{
|
|
window.Close();
|
|
}
|
|
}
|
|
|
|
[Fact]
|
|
public void SelectedItem_Is_Already_Updated_When_NodeSelected_Event_Raises()
|
|
{
|
|
var menu = new AtomUI.Desktop.Controls.NavMenu
|
|
{
|
|
Mode = NavMenuMode.Inline,
|
|
IsMotionEnabled = false
|
|
};
|
|
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();
|
|
Dispatcher.UIThread.RunJobs();
|
|
|
|
var secondContainer = menu.ContainerFromItem(second).ShouldBeOfType<NavMenuItem>();
|
|
|
|
INavMenuNode? observed = null;
|
|
menu.NavMenuNodeSelected += (_, _) => observed = menu.SelectedItem;
|
|
menu.SelectNavMenuItem(secondContainer);
|
|
|
|
observed.ShouldBeSameAs(second,
|
|
"NavMenuNodeSelected subscribers must observe the committed SelectedItem.");
|
|
menu.SelectedItem.ShouldBeSameAs(second);
|
|
}
|
|
finally
|
|
{
|
|
window.Close();
|
|
}
|
|
}
|
|
|
|
private static bool IsSelected(Control container)
|
|
{
|
|
return GetBoolProperty(container, "IsSelected");
|
|
}
|
|
|
|
private static bool GetBoolProperty(Control container, string propertyName)
|
|
{
|
|
var property = container.GetType().GetProperty(
|
|
propertyName,
|
|
BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic);
|
|
property.ShouldNotBeNull();
|
|
return (bool)property.GetValue(container)!;
|
|
}
|
|
|
|
private static BaseMotionActor GetChildItemsMotionActor(Control container)
|
|
{
|
|
var field = container.GetType().GetField(
|
|
"_childItemsLayoutTransform",
|
|
BindingFlags.Instance | BindingFlags.NonPublic);
|
|
field.ShouldNotBeNull();
|
|
return (BaseMotionActor)field.GetValue(container)!;
|
|
}
|
|
|
|
private static Border GetChildItemsFrame(Control container)
|
|
{
|
|
var actor = GetChildItemsMotionActor(container);
|
|
actor.Content.ShouldBeOfType<Border>();
|
|
return (Border)actor.Content;
|
|
}
|
|
|
|
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 void SetTimeSpanProperty(Control container, string propertyName, TimeSpan value)
|
|
{
|
|
var property = container.GetType().GetProperty(
|
|
propertyName,
|
|
BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic);
|
|
property.ShouldNotBeNull();
|
|
property.SetValue(container, value);
|
|
}
|
|
|
|
private static void MouseDown(Control control, Avalonia.Controls.Window window)
|
|
{
|
|
MouseMove(control, window);
|
|
window.MouseDown(GetCenterPoint(control, window), MouseButton.Left);
|
|
}
|
|
|
|
private static void MouseMove(Control control, Avalonia.Controls.Window window)
|
|
{
|
|
window.MouseMove(GetCenterPoint(control, window));
|
|
}
|
|
|
|
private static Point GetCenterPoint(Control control, Avalonia.Controls.Window window)
|
|
{
|
|
var point = control.TranslatePoint(
|
|
new Point(control.Bounds.Width / 2, control.Bounds.Height / 2),
|
|
window);
|
|
|
|
point.ShouldNotBeNull();
|
|
return point.Value;
|
|
}
|
|
|
|
private static Color? GetSolidBrushColor(IBrush? brush)
|
|
{
|
|
return brush is ISolidColorBrush solidColorBrush
|
|
? solidColorBrush.Color
|
|
: null;
|
|
}
|
|
|
|
private static void WaitForInlineMotionCompletion(BaseMotionActor actor)
|
|
{
|
|
var deadline = DateTime.UtcNow + TimeSpan.FromSeconds(2);
|
|
do
|
|
{
|
|
Dispatcher.UIThread.RunJobs();
|
|
AvaloniaHeadlessPlatform.ForceRenderTimerTick();
|
|
Dispatcher.UIThread.RunJobs();
|
|
if (!actor.IsAnimating(Visual.OpacityProperty))
|
|
{
|
|
return;
|
|
}
|
|
Thread.Sleep(10);
|
|
} while (DateTime.UtcNow < deadline);
|
|
|
|
actor.IsAnimating(Visual.OpacityProperty).ShouldBeFalse();
|
|
}
|
|
|
|
private static void DrainDispatcher()
|
|
{
|
|
Dispatcher.UIThread.RunJobs();
|
|
AvaloniaHeadlessPlatform.ForceRenderTimerTick();
|
|
Thread.Sleep(20);
|
|
AvaloniaHeadlessPlatform.ForceRenderTimerTick();
|
|
Dispatcher.UIThread.RunJobs();
|
|
}
|
|
|
|
private static void RunDispatcherJobsUntil(Func<bool> condition, int maxPasses = 128)
|
|
{
|
|
for (var i = 0; i < maxPasses && !condition(); i++)
|
|
{
|
|
Dispatcher.UIThread.RunJobs();
|
|
}
|
|
}
|
|
|
|
private static Avalonia.Controls.Primitives.VisualLayerManager CreatePopupOverlayHost(Control content)
|
|
{
|
|
var visualLayerManager = new Avalonia.Controls.Primitives.VisualLayerManager
|
|
{
|
|
Child = content
|
|
};
|
|
|
|
EnablePopupOverlayLayer(visualLayerManager);
|
|
return visualLayerManager;
|
|
}
|
|
|
|
private static void EnablePopupOverlayLayer(Avalonia.Controls.Primitives.VisualLayerManager visualLayerManager)
|
|
{
|
|
var property = typeof(Avalonia.Controls.Primitives.VisualLayerManager).GetProperty(
|
|
"EnablePopupOverlayLayer",
|
|
BindingFlags.Instance | BindingFlags.NonPublic);
|
|
|
|
property.ShouldNotBeNull();
|
|
property.SetValue(visualLayerManager, true);
|
|
}
|
|
}
|