mirror of
https://github.com/AtomUI/AtomUI.git
synced 2026-10-25 02:30:32 +08:00
1142 lines
41 KiB
C#
1142 lines
41 KiB
C#
using System.Reflection;
|
|
using System.Runtime.CompilerServices;
|
|
using AtomUI.Controls;
|
|
using AtomUI.Controls.Primitives;
|
|
using Avalonia;
|
|
using Avalonia.Controls;
|
|
using Avalonia.Controls.Primitives;
|
|
using Avalonia.Controls.Templates;
|
|
using Avalonia.Headless;
|
|
using Avalonia.Input;
|
|
using Avalonia.Interactivity;
|
|
using Avalonia.Layout;
|
|
using Avalonia.Media;
|
|
using Avalonia.Threading;
|
|
using Avalonia.VisualTree;
|
|
using Shouldly;
|
|
using Xunit;
|
|
using AvaloniaButton = Avalonia.Controls.Button;
|
|
using AvaloniaWindow = Avalonia.Controls.Window;
|
|
|
|
namespace AtomUI.Desktop.Controls.Tests.TabControl;
|
|
|
|
public class TabOverflowMenuTemplateTests
|
|
{
|
|
static TabOverflowMenuTemplateTests()
|
|
{
|
|
AvaloniaTestApp.EnsureInitialized();
|
|
}
|
|
|
|
public static TheoryData<OwnerKind> OwnerKinds =>
|
|
[
|
|
OwnerKind.TabControl,
|
|
OwnerKind.CardTabControl,
|
|
OwnerKind.TabStrip,
|
|
OwnerKind.CardTabStrip
|
|
];
|
|
|
|
// Shared TabScrollViewer behavior needs one representative of each owner base.
|
|
public static TheoryData<OwnerKind> BaseOwnerKinds =>
|
|
[
|
|
OwnerKind.TabControl,
|
|
OwnerKind.TabStrip
|
|
];
|
|
|
|
// Popup hosting, visual effects, and retention live in the shared viewer.
|
|
public static TheoryData<OwnerKind> SharedViewerOwner => [OwnerKind.TabControl];
|
|
|
|
// Each owner and placement keeps its own directional shadow and viewport checks.
|
|
public static TheoryData<OwnerKind, Dock> OwnerPlacementCases
|
|
{
|
|
get
|
|
{
|
|
var cases = new TheoryData<OwnerKind, Dock>();
|
|
foreach (var ownerKind in OwnerKinds)
|
|
{
|
|
cases.Add(ownerKind, Dock.Top);
|
|
cases.Add(ownerKind, Dock.Bottom);
|
|
cases.Add(ownerKind, Dock.Left);
|
|
cases.Add(ownerKind, Dock.Right);
|
|
}
|
|
return cases;
|
|
}
|
|
}
|
|
|
|
[Theory]
|
|
[MemberData(nameof(OwnerKinds))]
|
|
public void OverflowPopupTemplate_Defaults_To_Null_And_Is_Inherited(OwnerKind kind)
|
|
{
|
|
var owner = CreateOwner(kind);
|
|
GetOverflowPopupTemplate(owner).ShouldBeNull();
|
|
|
|
var template = new CapturingTemplate();
|
|
SetOverflowPopupTemplate(owner, template);
|
|
|
|
GetOverflowPopupTemplate(owner).ShouldBeSameAs(template);
|
|
}
|
|
|
|
[Theory]
|
|
[MemberData(nameof(OwnerKinds))]
|
|
public void Overflow_Snapshot_Preserves_Logical_Item_Header_Template_And_Effective_State(OwnerKind kind)
|
|
{
|
|
var headerTemplate = CreateHeaderTemplate();
|
|
var owner = CreateOwner(kind, headerTemplate, isClosable: true);
|
|
|
|
ShowInWindow(owner, window =>
|
|
{
|
|
var context = OpenOverflow(owner).OverflowPopupContext.ShouldNotBeNull();
|
|
|
|
context.Items.ShouldNotBeEmpty();
|
|
foreach (var item in context.Items)
|
|
{
|
|
var index = FindLogicalItemIndex(owner, item.Item);
|
|
index.ShouldBeGreaterThanOrEqualTo(0);
|
|
var container = GetContainer(owner, index);
|
|
item.Header.ShouldBeSameAs(GetHeader(container));
|
|
item.HeaderTemplate.ShouldBeSameAs(GetHeaderTemplate(container));
|
|
item.IsEnabled.ShouldBe(container.IsEffectivelyEnabled);
|
|
item.IsClosable.ShouldBe(GetIsClosable(container));
|
|
item.IsSelected.ShouldBe(index == GetSelectedIndex(owner));
|
|
}
|
|
});
|
|
}
|
|
|
|
[Theory]
|
|
[MemberData(nameof(OwnerKinds))]
|
|
public void Partially_Clipped_Item_Is_Included(OwnerKind kind)
|
|
{
|
|
var owner = CreateOwner(kind);
|
|
|
|
ShowInWindow(owner, window =>
|
|
{
|
|
var viewer = GetOverflowScrollViewer(owner);
|
|
viewer.Offset = new Vector(10, 0);
|
|
Dispatcher.UIThread.RunJobs();
|
|
|
|
OpenOverflow(owner).OverflowPopupContext!
|
|
.Items
|
|
.ShouldContain(item => ReferenceEquals(item.Item, GetLogicalItem(owner, 0)));
|
|
});
|
|
}
|
|
|
|
[Theory]
|
|
[MemberData(nameof(OwnerKinds))]
|
|
public void TryActivate_Uses_Current_Owner_Selection_And_Requires_Explicit_Dismiss(OwnerKind kind)
|
|
{
|
|
var owner = CreateOwner(kind);
|
|
|
|
ShowInWindow(owner, _ =>
|
|
{
|
|
var viewer = OpenOverflow(owner);
|
|
var context = viewer.OverflowPopupContext.ShouldNotBeNull();
|
|
var target = context.Items.Last(item => item.IsEnabled);
|
|
var targetIndex = FindLogicalItemIndex(owner, target.Item);
|
|
|
|
context.TryActivate(target).ShouldBeTrue();
|
|
GetSelectedIndex(owner).ShouldBe(targetIndex);
|
|
viewer.IsOverflowPopupOpen.ShouldBeTrue();
|
|
|
|
context.Dismiss();
|
|
viewer.IsOverflowPopupOpen.ShouldBeFalse();
|
|
context.Items.ShouldBeEmpty();
|
|
context.SelectedItem.ShouldBeNull();
|
|
context.TryActivate(target).ShouldBeFalse();
|
|
context.TryClose(target).ShouldBeFalse();
|
|
});
|
|
}
|
|
|
|
[Theory]
|
|
[MemberData(nameof(BaseOwnerKinds))]
|
|
public void Selection_Change_Republishes_Immutable_Snapshot(OwnerKind kind)
|
|
{
|
|
var owner = CreateOwner(kind);
|
|
|
|
ShowInWindow(owner, _ =>
|
|
{
|
|
var viewer = OpenOverflow(owner);
|
|
var context = viewer.OverflowPopupContext.ShouldNotBeNull();
|
|
var firstSnapshot = context.Items;
|
|
var target = firstSnapshot.Last();
|
|
var targetIndex = FindLogicalItemIndex(owner, target.Item);
|
|
var notifications = new List<string?>();
|
|
context.PropertyChanged += (_, args) => notifications.Add(args.PropertyName);
|
|
|
|
SetSelectedIndex(owner, targetIndex);
|
|
Dispatcher.UIThread.RunJobs();
|
|
|
|
context.Items.ShouldNotBeSameAs(firstSnapshot);
|
|
context.SelectedItem.ShouldNotBeNull();
|
|
context.SelectedItem!.Item.ShouldBeSameAs(target.Item);
|
|
notifications.ShouldContain(nameof(TabOverflowPopupContext.Items));
|
|
notifications.ShouldContain(nameof(TabOverflowPopupContext.SelectedItem));
|
|
});
|
|
}
|
|
|
|
[Theory]
|
|
[MemberData(nameof(BaseOwnerKinds))]
|
|
public void External_Collection_Mutation_Invalidates_And_Closes_Session(OwnerKind kind)
|
|
{
|
|
var owner = CreateOwner(kind);
|
|
|
|
ShowInWindow(owner, _ =>
|
|
{
|
|
var viewer = OpenOverflow(owner);
|
|
var context = viewer.OverflowPopupContext.ShouldNotBeNull();
|
|
var staleItem = context.Items.First();
|
|
|
|
AddItem(owner, "External mutation");
|
|
Dispatcher.UIThread.RunJobs();
|
|
|
|
viewer.IsOverflowPopupOpen.ShouldBeFalse();
|
|
context.Items.ShouldBeEmpty();
|
|
context.SelectedItem.ShouldBeNull();
|
|
context.TryActivate(staleItem).ShouldBeFalse();
|
|
});
|
|
}
|
|
|
|
[Theory]
|
|
[MemberData(nameof(BaseOwnerKinds))]
|
|
public void Successful_Close_Uses_Owner_Events_And_Invalidates_Session(OwnerKind kind)
|
|
{
|
|
var owner = CreateOwner(kind, isClosable: true);
|
|
var eventOrder = new List<string>();
|
|
AddCloseEventHandlers(owner, eventOrder, cancel: false);
|
|
|
|
ShowInWindow(owner, _ =>
|
|
{
|
|
var viewer = OpenOverflow(owner);
|
|
var context = viewer.OverflowPopupContext.ShouldNotBeNull();
|
|
var target = context.Items.First(item => item.IsClosable);
|
|
var oldCount = GetItemCount(owner);
|
|
|
|
context.TryClose(target).ShouldBeTrue();
|
|
|
|
eventOrder.ShouldBe(["closing", "closed"]);
|
|
GetItemCount(owner).ShouldBe(oldCount - 1);
|
|
viewer.IsOverflowPopupOpen.ShouldBeFalse();
|
|
context.Items.ShouldBeEmpty();
|
|
});
|
|
}
|
|
|
|
[Theory]
|
|
[MemberData(nameof(BaseOwnerKinds))]
|
|
public void Cancelled_Close_Leaves_Owner_And_Session_Untouched(OwnerKind kind)
|
|
{
|
|
var owner = CreateOwner(kind, isClosable: true);
|
|
var eventOrder = new List<string>();
|
|
AddCloseEventHandlers(owner, eventOrder, cancel: true);
|
|
|
|
ShowInWindow(owner, _ =>
|
|
{
|
|
var viewer = OpenOverflow(owner);
|
|
var context = viewer.OverflowPopupContext.ShouldNotBeNull();
|
|
var target = context.Items.First(item => item.IsClosable);
|
|
var oldItems = context.Items;
|
|
var oldCount = GetItemCount(owner);
|
|
|
|
context.TryClose(target).ShouldBeFalse();
|
|
|
|
eventOrder.ShouldBe(["closing"]);
|
|
GetItemCount(owner).ShouldBe(oldCount);
|
|
viewer.IsOverflowPopupOpen.ShouldBeTrue();
|
|
context.Items.ShouldBeSameAs(oldItems);
|
|
});
|
|
}
|
|
|
|
[Theory]
|
|
[MemberData(nameof(SharedViewerOwner))]
|
|
public void Custom_Template_Root_Is_Lazy_And_Reused_Between_Ordinary_Closes(OwnerKind kind)
|
|
{
|
|
var template = new CapturingTemplate();
|
|
var owner = CreateOwner(kind);
|
|
SetOverflowPopupTemplate(owner, template);
|
|
|
|
ShowInWindow(owner, _ =>
|
|
{
|
|
var viewer = GetOverflowScrollViewer(owner);
|
|
template.BuildCount.ShouldBe(0);
|
|
viewer.OverflowPopupContext.ShouldBeNull();
|
|
|
|
var firstContext = OpenOverflow(owner).OverflowPopupContext.ShouldNotBeNull();
|
|
var root = template.Root.ShouldNotBeNull();
|
|
template.BuildCount.ShouldBe(1);
|
|
|
|
firstContext.Dismiss();
|
|
Dispatcher.UIThread.RunJobs();
|
|
firstContext.Items.ShouldBeEmpty();
|
|
|
|
OpenOverflow(owner).OverflowPopupContext.ShouldBeSameAs(firstContext);
|
|
template.BuildCount.ShouldBe(1);
|
|
template.Root.ShouldBeSameAs(root);
|
|
});
|
|
}
|
|
|
|
[Theory]
|
|
[MemberData(nameof(SharedViewerOwner))]
|
|
public void Template_Replacement_Performs_Full_Teardown(OwnerKind kind)
|
|
{
|
|
var oldTemplate = new CapturingTemplate();
|
|
var newTemplate = new CapturingTemplate();
|
|
var owner = CreateOwner(kind);
|
|
SetOverflowPopupTemplate(owner, oldTemplate);
|
|
|
|
ShowInWindow(owner, _ =>
|
|
{
|
|
var viewer = OpenOverflow(owner);
|
|
var oldContext = viewer.OverflowPopupContext.ShouldNotBeNull();
|
|
var oldRoot = oldTemplate.Root.ShouldNotBeNull();
|
|
|
|
SetOverflowPopupTemplate(owner, newTemplate);
|
|
Dispatcher.UIThread.RunJobs();
|
|
|
|
viewer.IsOverflowPopupOpen.ShouldBeFalse();
|
|
viewer.OverflowPopupContext.ShouldBeNull();
|
|
oldContext.Items.ShouldBeEmpty();
|
|
oldRoot.GetVisualParent().ShouldBeNull();
|
|
|
|
OpenOverflow(owner);
|
|
newTemplate.BuildCount.ShouldBe(1);
|
|
newTemplate.Root.ShouldNotBeSameAs(oldRoot);
|
|
});
|
|
}
|
|
|
|
[Theory]
|
|
[InlineData(Dock.Top, PlacementMode.BottomEdgeAlignedLeft)]
|
|
[InlineData(Dock.Bottom, PlacementMode.TopEdgeAlignedLeft)]
|
|
[InlineData(Dock.Right, PlacementMode.LeftEdgeAlignedBottom)]
|
|
[InlineData(Dock.Left, PlacementMode.RightEdgeAlignedBottom)]
|
|
public void Popup_Placement_Follows_TabStrip_Edge(Dock dock, PlacementMode expected)
|
|
{
|
|
var owner = CreateOwner(OwnerKind.TabControl, placement: dock);
|
|
|
|
ShowInWindow(owner, _ =>
|
|
{
|
|
var viewer = GetOverflowScrollViewer(owner);
|
|
viewer.OverflowPopupPlacement.ShouldBe(expected);
|
|
OpenOverflow(owner).OverflowPopup!.RequestedPlacement.ShouldBe(expected);
|
|
});
|
|
}
|
|
|
|
[Theory]
|
|
[MemberData(nameof(OwnerPlacementCases))]
|
|
public void Overflow_Edge_Indicators_Use_Directional_Outer_Shadows(
|
|
OwnerKind kind,
|
|
Dock placement)
|
|
{
|
|
var owner = CreateOwner(kind, placement: placement);
|
|
var isHorizontal = placement is Dock.Top or Dock.Bottom;
|
|
|
|
ShowInWindow(owner, _ =>
|
|
{
|
|
var viewer = GetOverflowScrollViewer(owner);
|
|
var presenter = viewer.GetVisualDescendants()
|
|
.OfType<TabScrollContentPresenter>()
|
|
.ShouldHaveSingleItem();
|
|
var startIndicator = viewer.GetVisualDescendants()
|
|
.OfType<TabOverflowEdgeIndicator>()
|
|
.Single(border => border.Name == "PART_ScrollStartEdgeIndicator");
|
|
var endIndicator = viewer.GetVisualDescendants()
|
|
.OfType<TabOverflowEdgeIndicator>()
|
|
.Single(border => border.Name == "PART_ScrollEndEdgeIndicator");
|
|
|
|
AssertDirectionalOuterShadow(startIndicator, isHorizontal, isStart: true);
|
|
AssertDirectionalOuterShadow(endIndicator, isHorizontal, isStart: false);
|
|
|
|
var startBounds = TransformBounds(startIndicator, presenter);
|
|
var endBounds = TransformBounds(endIndicator, presenter);
|
|
if (isHorizontal)
|
|
{
|
|
startBounds.Right.ShouldBe(0, 0.5);
|
|
endBounds.Left.ShouldBe(presenter.Bounds.Width, 0.5);
|
|
startIndicator.Width.ShouldBe(32, 0.5);
|
|
endIndicator.Width.ShouldBe(32, 0.5);
|
|
}
|
|
else
|
|
{
|
|
startBounds.Bottom.ShouldBe(0, 0.5);
|
|
endBounds.Top.ShouldBe(presenter.Bounds.Height, 0.5);
|
|
startIndicator.Height.ShouldBe(32, 0.5);
|
|
endIndicator.Height.ShouldBe(32, 0.5);
|
|
}
|
|
|
|
startIndicator.IsVisible.ShouldBeFalse();
|
|
endIndicator.IsVisible.ShouldBeTrue();
|
|
|
|
viewer.Offset = isHorizontal
|
|
? new Vector(viewer.ScrollBarMaximum.X, 0)
|
|
: new Vector(0, viewer.ScrollBarMaximum.Y);
|
|
Dispatcher.UIThread.RunJobs();
|
|
|
|
startIndicator.IsVisible.ShouldBeTrue();
|
|
endIndicator.IsVisible.ShouldBeFalse();
|
|
});
|
|
}
|
|
|
|
[Theory]
|
|
[InlineData(Dock.Top)]
|
|
[InlineData(Dock.Bottom)]
|
|
[InlineData(Dock.Left)]
|
|
[InlineData(Dock.Right)]
|
|
public void Visible_End_Edge_Shadow_Uses_A_Viewport_Clipped_Input_Transparent_Layer(Dock placement)
|
|
{
|
|
var owner = CreateOwner(OwnerKind.TabControl, placement: placement);
|
|
var isHorizontal = placement is Dock.Top or Dock.Bottom;
|
|
|
|
ShowInWindow(owner, window =>
|
|
{
|
|
var viewer = GetOverflowScrollViewer(owner);
|
|
var presenter = viewer.GetVisualDescendants()
|
|
.OfType<TabScrollContentPresenter>()
|
|
.ShouldHaveSingleItem();
|
|
var endIndicator = viewer.GetVisualDescendants()
|
|
.OfType<TabOverflowEdgeIndicator>()
|
|
.Single(border => border.Name == "PART_ScrollEndEdgeIndicator");
|
|
|
|
var indicatorBounds = TransformBounds(endIndicator, window);
|
|
var presenterBounds = TransformBounds(presenter, window);
|
|
endIndicator.IsHitTestVisible.ShouldBeFalse();
|
|
var layer = endIndicator.GetVisualParent().ShouldBeOfType<Canvas>();
|
|
layer.ClipToBounds.ShouldBeTrue();
|
|
layer.IsHitTestVisible.ShouldBeFalse();
|
|
TransformBounds(layer, window).ShouldBe(presenterBounds);
|
|
|
|
var shadow = endIndicator.BoxShadow[0];
|
|
if (isHorizontal)
|
|
{
|
|
shadow.OffsetX.ShouldBeLessThan(0);
|
|
indicatorBounds.Left.ShouldBe(presenterBounds.Right, 0.5);
|
|
(indicatorBounds.Left - 12).ShouldBeGreaterThanOrEqualTo(presenterBounds.Left);
|
|
}
|
|
else
|
|
{
|
|
shadow.OffsetY.ShouldBeLessThan(0);
|
|
indicatorBounds.Top.ShouldBe(presenterBounds.Bottom, 0.5);
|
|
(indicatorBounds.Top - 12).ShouldBeGreaterThanOrEqualTo(presenterBounds.Top);
|
|
}
|
|
});
|
|
}
|
|
|
|
[Theory]
|
|
[MemberData(nameof(OwnerPlacementCases))]
|
|
public void Overflow_Shadow_Casters_Follow_The_Viewport_Boundary(OwnerKind kind, Dock placement)
|
|
{
|
|
var owner = CreateOwner(kind, placement: placement);
|
|
ShowInWindow(owner, window =>
|
|
{
|
|
var oppositeAxis = placement is Dock.Top or Dock.Bottom ? Dock.Left : Dock.Top;
|
|
foreach (var currentPlacement in new[] { placement, oppositeAxis, placement })
|
|
{
|
|
if (owner is BaseTabControl tabControl) tabControl.TabStripPlacement = currentPlacement;
|
|
else ((BaseTabStrip)owner).TabStripPlacement = currentPlacement;
|
|
Dispatcher.UIThread.RunJobs();
|
|
window.UpdateLayout();
|
|
var viewer = GetOverflowScrollViewer(owner);
|
|
var viewport = viewer.GetVisualDescendants().OfType<Panel>()
|
|
.Single(control => control.Name == "ScrollContentViewport");
|
|
var start = viewer.GetVisualDescendants().OfType<TabOverflowEdgeIndicator>()
|
|
.Single(control => control.Name == "PART_ScrollStartEdgeIndicator");
|
|
var end = viewer.GetVisualDescendants().OfType<TabOverflowEdgeIndicator>()
|
|
.Single(control => control.Name == "PART_ScrollEndEdgeIndicator");
|
|
var horizontal = currentPlacement is Dock.Top or Dock.Bottom;
|
|
viewer.Offset = horizontal ? new Vector(viewer.ScrollBarMaximum.X / 2, 0)
|
|
: new Vector(0, viewer.ScrollBarMaximum.Y / 2);
|
|
Dispatcher.UIThread.RunJobs();
|
|
window.UpdateLayout();
|
|
start.IsVisible.ShouldBeTrue();
|
|
end.IsVisible.ShouldBeTrue();
|
|
var startBounds = TransformBounds(start, viewport);
|
|
var endBounds = TransformBounds(end, viewport);
|
|
// Outer shadows exclude the caster. Its inward face must meet the
|
|
// scroll boundary even after switching axes on the same owner.
|
|
if (horizontal)
|
|
{
|
|
startBounds.Right.ShouldBe(0, 0.5);
|
|
endBounds.Left.ShouldBe(viewport.Bounds.Width, 0.5);
|
|
startBounds.Height.ShouldBe(viewport.Bounds.Height, 0.5);
|
|
endBounds.Height.ShouldBe(viewport.Bounds.Height, 0.5);
|
|
}
|
|
else
|
|
{
|
|
startBounds.Bottom.ShouldBe(0, 0.5);
|
|
endBounds.Top.ShouldBe(viewport.Bounds.Height, 0.5);
|
|
startBounds.Width.ShouldBe(viewport.Bounds.Width, 0.5);
|
|
endBounds.Width.ShouldBe(viewport.Bounds.Width, 0.5);
|
|
}
|
|
}
|
|
});
|
|
}
|
|
|
|
[Theory]
|
|
[MemberData(nameof(SharedViewerOwner))]
|
|
public void Default_Overflow_Menu_Hides_The_Scrollbar_But_Remains_Scrollable(OwnerKind kind)
|
|
{
|
|
var owner = CreateOwner(kind);
|
|
for (var index = 0; index < 20; index++)
|
|
{
|
|
AddItem(owner, $"Additional document {index:00}");
|
|
}
|
|
|
|
ShowInWindow(owner, window =>
|
|
{
|
|
var menu = OpenOverflow(owner).OverflowPopupRoot.ShouldBeOfType<TabOverflowMenu>();
|
|
var scrollViewer = menu.GetVisualDescendants()
|
|
.OfType<AtomUI.Desktop.Controls.ScrollViewer>()
|
|
.ShouldHaveSingleItem();
|
|
var verticalScrollBar = scrollViewer.GetVisualDescendants()
|
|
.OfType<AtomUI.Desktop.Controls.ScrollBar>()
|
|
.Single(item => item.Orientation == Orientation.Vertical);
|
|
|
|
scrollViewer.VerticalScrollBarVisibility.ShouldBe(ScrollBarVisibility.Hidden);
|
|
verticalScrollBar.IsVisible.ShouldBeFalse();
|
|
scrollViewer.ScrollBarMaximum.Y.ShouldBeGreaterThan(0);
|
|
|
|
var wheelPoint = scrollViewer.TranslatePoint(
|
|
new Point(scrollViewer.Bounds.Width / 2, scrollViewer.Bounds.Height / 2),
|
|
window);
|
|
wheelPoint.ShouldNotBeNull();
|
|
window.MouseMove(wheelPoint.Value);
|
|
window.MouseWheel(wheelPoint.Value, new Vector(0, -1), RawInputModifiers.None);
|
|
Dispatcher.UIThread.RunJobs();
|
|
window.UpdateLayout();
|
|
scrollViewer.Offset.Y.ShouldBeGreaterThan(0);
|
|
|
|
scrollViewer.Offset = new Vector(0, scrollViewer.ScrollBarMaximum.Y);
|
|
Dispatcher.UIThread.RunJobs();
|
|
scrollViewer.Offset.Y.ShouldBeGreaterThan(0);
|
|
});
|
|
}
|
|
|
|
private static void AssertDirectionalOuterShadow(TabOverflowEdgeIndicator indicator, bool isHorizontal, bool isStart)
|
|
{
|
|
indicator.BoxShadow.Count.ShouldBe(1);
|
|
var shadow = indicator.BoxShadow[0];
|
|
shadow.IsInset.ShouldBeFalse();
|
|
shadow.Blur.ShouldBe(8);
|
|
shadow.Spread.ShouldBe(-8);
|
|
shadow.Color.ShouldBe(Color.FromArgb(20, 0, 0, 0));
|
|
shadow.OffsetX.ShouldBe(isHorizontal ? (isStart ? 10 : -10) : 0);
|
|
shadow.OffsetY.ShouldBe(isHorizontal ? 0 : (isStart ? 10 : -10));
|
|
}
|
|
|
|
private static Rect TransformBounds(Control control, Visual relativeTo)
|
|
{
|
|
var transform = control.TransformToVisual(relativeTo).ShouldNotBeNull();
|
|
return new Rect(control.Bounds.Size).TransformToAABB(transform);
|
|
}
|
|
|
|
|
|
[Theory]
|
|
[MemberData(nameof(SharedViewerOwner))]
|
|
public void Detach_Performs_Full_Teardown_Regardless_Of_Pinned_State(OwnerKind kind)
|
|
{
|
|
var template = new CapturingTemplate();
|
|
var owner = CreateOwner(kind);
|
|
SetOverflowPopupTemplate(owner, template);
|
|
SetPinnedOpen(owner, true);
|
|
|
|
ShowInWindowWithHost(owner, (_, host) =>
|
|
{
|
|
var viewer = GetOverflowScrollViewer(owner);
|
|
var context = viewer.OverflowPopupContext.ShouldNotBeNull();
|
|
var root = template.Root.ShouldNotBeNull();
|
|
|
|
host.Children.Remove(owner);
|
|
Dispatcher.UIThread.RunJobs();
|
|
|
|
viewer.IsOverflowPopupOpen.ShouldBeFalse();
|
|
viewer.OverflowPopupContext.ShouldBeNull();
|
|
viewer.OverflowOwner.ShouldBeNull();
|
|
context.Items.ShouldBeEmpty();
|
|
root.GetVisualParent().ShouldBeNull();
|
|
});
|
|
}
|
|
|
|
[Theory]
|
|
[MemberData(nameof(OwnerKinds))]
|
|
public void Default_Template_Uses_Shared_Overflow_Menu(OwnerKind kind)
|
|
{
|
|
var owner = CreateOwner(kind);
|
|
|
|
ShowInWindow(owner, _ =>
|
|
{
|
|
var viewer = OpenOverflow(owner);
|
|
viewer.OverflowPopupRoot.ShouldBeOfType<TabOverflowMenu>();
|
|
viewer.OverflowPopupRoot!.GetVisualDescendants()
|
|
.OfType<TabOverflowMenuItem>()
|
|
.ShouldNotBeEmpty();
|
|
});
|
|
}
|
|
|
|
[Theory]
|
|
[MemberData(nameof(SharedViewerOwner))]
|
|
public void Default_Overflow_Surface_Uses_The_Menu_Corner_Radius(OwnerKind kind)
|
|
{
|
|
var owner = CreateOwner(kind);
|
|
|
|
ShowInWindow(owner, _ =>
|
|
{
|
|
var menu = OpenOverflow(owner).OverflowPopupRoot.ShouldBeOfType<TabOverflowMenu>();
|
|
var shadowContainer = menu.GetVisualAncestors()
|
|
.OfType<ShadowsAwareContainer>()
|
|
.ShouldHaveSingleItem();
|
|
|
|
menu.CornerRadius.TopLeft.ShouldBeGreaterThan(0);
|
|
shadowContainer.CornerRadius.ShouldBe(menu.CornerRadius);
|
|
});
|
|
}
|
|
|
|
[Theory]
|
|
[MemberData(nameof(BaseOwnerKinds))]
|
|
public void Opening_Overflow_From_Pointer_Does_Not_Move_Focus_Into_The_Popup(OwnerKind kind)
|
|
{
|
|
var owner = CreateOwner(kind);
|
|
|
|
ShowInWindow(owner, _ =>
|
|
{
|
|
var viewer = GetOverflowScrollViewer(owner);
|
|
var indicator = GetMenuIndicator(viewer);
|
|
indicator.Focus();
|
|
TopLevel.GetTopLevel(owner)!.FocusManager.GetFocusedElement().ShouldBeSameAs(indicator);
|
|
|
|
indicator.RaiseEvent(new RoutedEventArgs(AvaloniaButton.ClickEvent, indicator));
|
|
Dispatcher.UIThread.RunJobs();
|
|
|
|
viewer.IsOverflowPopupOpen.ShouldBeTrue();
|
|
TopLevel.GetTopLevel(owner)!.FocusManager.GetFocusedElement().ShouldBeSameAs(indicator);
|
|
});
|
|
}
|
|
|
|
[Theory]
|
|
[MemberData(nameof(OwnerKinds))]
|
|
public void Menu_Indicator_Is_Reserved_When_Final_Arrange_Is_Narrower_Than_Measure(OwnerKind kind)
|
|
{
|
|
var owner = CreateOwner(kind);
|
|
while (GetItemCount(owner) > 4)
|
|
{
|
|
RemoveLastItem(owner);
|
|
}
|
|
owner.Width = double.NaN;
|
|
var host = new ScopeAwareOverlayLayerPanel { Width = 900, Height = 240 };
|
|
host.Children.Add(owner);
|
|
var window = new AvaloniaWindow
|
|
{
|
|
Width = 900,
|
|
Height = 240,
|
|
Content = CreatePopupOverlayHost(host)
|
|
};
|
|
|
|
try
|
|
{
|
|
window.Show();
|
|
Dispatcher.UIThread.RunJobs();
|
|
|
|
var viewer = owner.GetVisualDescendants().OfType<TabScrollViewer>().ShouldHaveSingleItem();
|
|
GetMenuIndicator(viewer).IsVisible.ShouldBeFalse();
|
|
|
|
owner.Measure(new Size(900, 240));
|
|
owner.Arrange(new Rect(0, 0, 220, 240));
|
|
|
|
var indicator = GetMenuIndicator(viewer);
|
|
indicator.IsVisible.ShouldBeTrue();
|
|
var transform = indicator.TransformToVisual(owner).ShouldNotBeNull();
|
|
var bounds = new Rect(indicator.Bounds.Size).TransformToAABB(transform);
|
|
bounds.Right.ShouldBeLessThanOrEqualTo(owner.Bounds.Width + 0.5);
|
|
}
|
|
finally
|
|
{
|
|
window.Close();
|
|
Dispatcher.UIThread.RunJobs();
|
|
}
|
|
}
|
|
|
|
[Theory]
|
|
[MemberData(nameof(BaseOwnerKinds))]
|
|
public void Default_Menu_Keyboard_Activation_Disabled_State_And_Escape_Are_Session_Safe(OwnerKind kind)
|
|
{
|
|
var owner = CreateOwner(kind);
|
|
|
|
ShowInWindow(owner, _ =>
|
|
{
|
|
var viewer = OpenOverflow(owner);
|
|
var context = viewer.OverflowPopupContext.ShouldNotBeNull();
|
|
var disabled = context.Items.Single(item => !item.IsEnabled);
|
|
context.TryActivate(disabled).ShouldBeFalse();
|
|
viewer.IsOverflowPopupOpen.ShouldBeTrue();
|
|
|
|
var target = context.Items.Last(item => item.IsEnabled);
|
|
var targetIndex = FindLogicalItemIndex(owner, target.Item);
|
|
var menu = viewer.OverflowPopupRoot.ShouldBeOfType<TabOverflowMenu>();
|
|
var menuItem = menu.GetVisualDescendants()
|
|
.OfType<TabOverflowMenuItem>()
|
|
.Single(item => ReferenceEquals(item.OverflowItem, target));
|
|
|
|
menuItem.RaiseEvent(new KeyEventArgs
|
|
{
|
|
RoutedEvent = InputElement.KeyDownEvent,
|
|
Source = menuItem,
|
|
Key = Key.Enter
|
|
});
|
|
Dispatcher.UIThread.RunJobs();
|
|
|
|
GetSelectedIndex(owner).ShouldBe(targetIndex);
|
|
viewer.IsOverflowPopupOpen.ShouldBeFalse();
|
|
|
|
menu = OpenOverflow(owner).OverflowPopupRoot.ShouldBeOfType<TabOverflowMenu>();
|
|
menu.RaiseEvent(new KeyEventArgs
|
|
{
|
|
RoutedEvent = InputElement.KeyDownEvent,
|
|
Source = menu,
|
|
Key = Key.Escape
|
|
});
|
|
Dispatcher.UIThread.RunJobs();
|
|
viewer.IsOverflowPopupOpen.ShouldBeFalse();
|
|
});
|
|
}
|
|
|
|
[Theory]
|
|
[MemberData(nameof(SharedViewerOwner))]
|
|
public void Repeated_Ordinary_Open_Close_Reuses_One_Context_And_Visual_Root(OwnerKind kind)
|
|
{
|
|
var owner = CreateOwner(kind);
|
|
|
|
ShowInWindow(owner, _ =>
|
|
{
|
|
var viewer = OpenOverflow(owner);
|
|
var context = viewer.OverflowPopupContext.ShouldNotBeNull();
|
|
var root = viewer.OverflowPopupRoot.ShouldNotBeNull();
|
|
var openVisualCount = root.GetVisualDescendants().Count() + 1;
|
|
context.Dismiss();
|
|
Dispatcher.UIThread.RunJobs();
|
|
|
|
for (var iteration = 0; iteration < 40; iteration++)
|
|
{
|
|
OpenOverflow(owner);
|
|
viewer.OverflowPopupContext.ShouldBeSameAs(context);
|
|
viewer.OverflowPopupRoot.ShouldBeSameAs(root);
|
|
(root.GetVisualDescendants().Count() + 1).ShouldBe(openVisualCount);
|
|
|
|
context.Dismiss();
|
|
Dispatcher.UIThread.RunJobs();
|
|
context.Items.ShouldBeEmpty();
|
|
context.SelectedItem.ShouldBeNull();
|
|
}
|
|
});
|
|
}
|
|
|
|
[Fact]
|
|
public void Saved_Closed_Context_Does_Not_Retain_Action_Target_Or_Its_Owner_Graph()
|
|
{
|
|
var release = CreateClosedContextTargetReference();
|
|
|
|
CollectGarbage();
|
|
|
|
release.Target.IsAlive.ShouldBeFalse();
|
|
release.OwnerGraph.IsAlive.ShouldBeFalse();
|
|
release.Context.Items.ShouldBeEmpty();
|
|
release.Context.SelectedItem.ShouldBeNull();
|
|
}
|
|
|
|
[Theory]
|
|
[MemberData(nameof(SharedViewerOwner))]
|
|
public void Detached_Opened_Owner_Popup_And_Context_Are_Collectible(OwnerKind kind)
|
|
{
|
|
var references = CreateDetachedOverflowGraphReferences(kind);
|
|
|
|
CollectGarbage();
|
|
|
|
references.Owner.IsAlive.ShouldBeFalse();
|
|
references.Viewer.IsAlive.ShouldBeFalse();
|
|
references.Context.IsAlive.ShouldBeFalse();
|
|
references.PopupRoot.IsAlive.ShouldBeFalse();
|
|
}
|
|
|
|
private static Control CreateOwner(
|
|
OwnerKind kind,
|
|
IDataTemplate? headerTemplate = null,
|
|
bool isClosable = false,
|
|
Dock placement = Dock.Top)
|
|
{
|
|
if (kind is OwnerKind.TabControl or OwnerKind.CardTabControl)
|
|
{
|
|
BaseTabControl owner = kind == OwnerKind.CardTabControl
|
|
? new CardTabControl()
|
|
: new AtomUI.Desktop.Controls.TabControl();
|
|
owner.Width = placement is Dock.Top or Dock.Bottom ? 220 : 360;
|
|
owner.Height = placement is Dock.Left or Dock.Right ? 160 : 240;
|
|
owner.SelectedIndex = 0;
|
|
owner.IsTabClosable = isClosable;
|
|
owner.IsMotionEnabled = false;
|
|
owner.TabStripPlacement = placement;
|
|
for (var i = 0; i < 12; i++)
|
|
{
|
|
owner.Items.Add(new AtomUI.Desktop.Controls.TabItem
|
|
{
|
|
Header = $"Document {i + 1:00}",
|
|
HeaderTemplate = headerTemplate,
|
|
Content = $"Content {i + 1:00}",
|
|
IsClosable = isClosable && i % 2 == 0,
|
|
IsEnabled = i != 9
|
|
});
|
|
}
|
|
return owner;
|
|
}
|
|
|
|
BaseTabStrip strip = kind == OwnerKind.CardTabStrip
|
|
? new CardTabStrip()
|
|
: new AtomUI.Desktop.Controls.TabStrip();
|
|
strip.Width = placement is Dock.Top or Dock.Bottom ? 220 : 360;
|
|
strip.Height = placement is Dock.Left or Dock.Right ? 160 : 240;
|
|
strip.SelectedIndex = 0;
|
|
strip.IsTabClosable = isClosable;
|
|
strip.IsMotionEnabled = false;
|
|
strip.TabStripPlacement = placement;
|
|
for (var i = 0; i < 12; i++)
|
|
{
|
|
strip.Items.Add(new TabStripItem
|
|
{
|
|
Content = $"Document {i + 1:00}",
|
|
ContentTemplate = headerTemplate,
|
|
IsClosable = isClosable && i % 2 == 0,
|
|
IsEnabled = i != 9
|
|
});
|
|
}
|
|
return strip;
|
|
}
|
|
|
|
private static TabScrollViewer OpenOverflow(Control owner)
|
|
{
|
|
var viewer = GetOverflowScrollViewer(owner);
|
|
var indicator = GetMenuIndicator(viewer);
|
|
indicator.IsVisible.ShouldBeTrue();
|
|
indicator.RaiseEvent(new RoutedEventArgs(AvaloniaButton.ClickEvent, indicator));
|
|
Dispatcher.UIThread.RunJobs();
|
|
viewer.IsOverflowPopupOpen.ShouldBeTrue();
|
|
return viewer;
|
|
}
|
|
|
|
private static IconButton GetMenuIndicator(TabScrollViewer viewer)
|
|
{
|
|
return viewer.GetVisualDescendants()
|
|
.OfType<IconButton>()
|
|
.Single(button => button.Name == "PART_ScrollMenuIndicator");
|
|
}
|
|
|
|
private static TabScrollViewer GetOverflowScrollViewer(Control owner)
|
|
{
|
|
var viewer = owner.GetVisualDescendants().OfType<TabScrollViewer>().ShouldHaveSingleItem();
|
|
if (viewer.TabStripPlacement is Dock.Top or Dock.Bottom)
|
|
{
|
|
viewer.Extent.Width.ShouldBeGreaterThan(viewer.Viewport.Width);
|
|
}
|
|
else
|
|
{
|
|
viewer.Extent.Height.ShouldBeGreaterThan(viewer.Viewport.Height);
|
|
}
|
|
return viewer;
|
|
}
|
|
|
|
private static object? GetLogicalItem(Control owner, int index) => owner switch
|
|
{
|
|
BaseTabControl tabControl => tabControl.ItemsView[index],
|
|
BaseTabStrip tabStrip => tabStrip.ItemsView[index],
|
|
_ => throw new ArgumentOutOfRangeException(nameof(owner))
|
|
};
|
|
|
|
private static int FindLogicalItemIndex(Control owner, object? item) => owner switch
|
|
{
|
|
BaseTabControl tabControl => tabControl.ItemsView.IndexOf(item),
|
|
BaseTabStrip tabStrip => tabStrip.ItemsView.IndexOf(item),
|
|
_ => -1
|
|
};
|
|
|
|
private static Control GetContainer(Control owner, int index) => owner switch
|
|
{
|
|
BaseTabControl tabControl => tabControl.ContainerFromIndex(index).ShouldNotBeNull(),
|
|
BaseTabStrip tabStrip => tabStrip.ContainerFromIndex(index).ShouldNotBeNull(),
|
|
_ => throw new ArgumentOutOfRangeException(nameof(owner))
|
|
};
|
|
|
|
private static object? GetHeader(Control container) => container switch
|
|
{
|
|
AtomUI.Desktop.Controls.TabItem item => item.Header,
|
|
TabStripItem item => item.Content,
|
|
_ => null
|
|
};
|
|
|
|
private static IDataTemplate? GetHeaderTemplate(Control container) => container switch
|
|
{
|
|
AtomUI.Desktop.Controls.TabItem item => item.HeaderTemplate,
|
|
TabStripItem item => item.ContentTemplate,
|
|
_ => null
|
|
};
|
|
|
|
private static bool GetIsClosable(Control container) => container switch
|
|
{
|
|
AtomUI.Desktop.Controls.TabItem item => item.IsClosable,
|
|
TabStripItem item => item.IsClosable,
|
|
_ => false
|
|
};
|
|
|
|
private static int GetSelectedIndex(Control owner) => owner switch
|
|
{
|
|
BaseTabControl tabControl => tabControl.SelectedIndex,
|
|
BaseTabStrip tabStrip => tabStrip.SelectedIndex,
|
|
_ => -1
|
|
};
|
|
|
|
private static void SetSelectedIndex(Control owner, int index)
|
|
{
|
|
switch (owner)
|
|
{
|
|
case BaseTabControl tabControl:
|
|
tabControl.SelectedIndex = index;
|
|
break;
|
|
case BaseTabStrip tabStrip:
|
|
tabStrip.SelectedIndex = index;
|
|
break;
|
|
}
|
|
}
|
|
|
|
private static int GetItemCount(Control owner) => owner switch
|
|
{
|
|
BaseTabControl tabControl => tabControl.ItemCount,
|
|
BaseTabStrip tabStrip => tabStrip.ItemCount,
|
|
_ => 0
|
|
};
|
|
|
|
private static void AddItem(Control owner, string text)
|
|
{
|
|
switch (owner)
|
|
{
|
|
case BaseTabControl tabControl:
|
|
tabControl.Items.Add(new AtomUI.Desktop.Controls.TabItem { Header = text, Content = text });
|
|
break;
|
|
case BaseTabStrip tabStrip:
|
|
tabStrip.Items.Add(new TabStripItem { Content = text });
|
|
break;
|
|
}
|
|
}
|
|
|
|
private static void RemoveLastItem(Control owner)
|
|
{
|
|
switch (owner)
|
|
{
|
|
case BaseTabControl tabControl:
|
|
tabControl.Items.RemoveAt(tabControl.ItemCount - 1);
|
|
break;
|
|
case BaseTabStrip tabStrip:
|
|
tabStrip.Items.RemoveAt(tabStrip.ItemCount - 1);
|
|
break;
|
|
}
|
|
}
|
|
|
|
private static IDataTemplate? GetOverflowPopupTemplate(Control owner) => owner switch
|
|
{
|
|
BaseTabControl tabControl => tabControl.OverflowPopupTemplate,
|
|
BaseTabStrip tabStrip => tabStrip.OverflowPopupTemplate,
|
|
_ => null
|
|
};
|
|
|
|
private static void SetOverflowPopupTemplate(Control owner, IDataTemplate template)
|
|
{
|
|
switch (owner)
|
|
{
|
|
case BaseTabControl tabControl:
|
|
tabControl.OverflowPopupTemplate = template;
|
|
break;
|
|
case BaseTabStrip tabStrip:
|
|
tabStrip.OverflowPopupTemplate = template;
|
|
break;
|
|
}
|
|
}
|
|
|
|
private static void SetPinnedOpen(Control owner, bool value)
|
|
{
|
|
switch (owner)
|
|
{
|
|
case BaseTabControl tabControl:
|
|
tabControl.IsPopupPinnedOpen = value;
|
|
break;
|
|
case BaseTabStrip tabStrip:
|
|
tabStrip.IsPopupPinnedOpen = value;
|
|
break;
|
|
}
|
|
}
|
|
|
|
private static void AddCloseEventHandlers(Control owner, ICollection<string> order, bool cancel)
|
|
{
|
|
if (owner is BaseTabControl tabControl)
|
|
{
|
|
tabControl.Closing += (_, args) =>
|
|
{
|
|
order.Add("closing");
|
|
args.Cancel = cancel;
|
|
};
|
|
tabControl.Closed += (_, _) => order.Add("closed");
|
|
}
|
|
else if (owner is BaseTabStrip tabStrip)
|
|
{
|
|
tabStrip.Closing += (_, args) =>
|
|
{
|
|
order.Add("closing");
|
|
args.Cancel = cancel;
|
|
};
|
|
tabStrip.Closed += (_, _) => order.Add("closed");
|
|
}
|
|
}
|
|
|
|
private static FuncDataTemplate<string> CreateHeaderTemplate()
|
|
{
|
|
return new FuncDataTemplate<string>((item, _) => new TextBlock { Text = item });
|
|
}
|
|
|
|
private static void ShowInWindow(Control content, Action<AvaloniaWindow> assertion)
|
|
{
|
|
ShowInWindowWithHost(content, (window, _) => assertion(window));
|
|
}
|
|
|
|
private static void ShowInWindowWithHost(
|
|
Control content,
|
|
Action<AvaloniaWindow, ScopeAwareOverlayLayerPanel> assertion)
|
|
{
|
|
var host = new ScopeAwareOverlayLayerPanel { Width = 420, Height = 320 };
|
|
host.Children.Add(content);
|
|
var window = new AvaloniaWindow
|
|
{
|
|
Width = 420,
|
|
Height = 320,
|
|
Content = CreatePopupOverlayHost(host)
|
|
};
|
|
|
|
try
|
|
{
|
|
window.Show();
|
|
Dispatcher.UIThread.RunJobs();
|
|
assertion(window, host);
|
|
}
|
|
finally
|
|
{
|
|
window.Close();
|
|
Dispatcher.UIThread.RunJobs();
|
|
}
|
|
}
|
|
|
|
[MethodImpl(MethodImplOptions.NoInlining)]
|
|
private static ContextTargetReleaseResult CreateClosedContextTargetReference()
|
|
{
|
|
var context = new TabOverflowPopupContext();
|
|
var ownerGraph = new object();
|
|
var target = new RetainingActionTarget(ownerGraph);
|
|
var targetReference = new WeakReference(target);
|
|
var ownerGraphReference = new WeakReference(ownerGraph);
|
|
|
|
context.OpenSession(1, Array.Empty<TabOverflowItem>(), null, target);
|
|
context.CloseSession();
|
|
|
|
return new ContextTargetReleaseResult(context, targetReference, ownerGraphReference);
|
|
}
|
|
|
|
[MethodImpl(MethodImplOptions.NoInlining)]
|
|
private static OverflowGraphReferences CreateDetachedOverflowGraphReferences(OwnerKind kind)
|
|
{
|
|
var template = new CapturingTemplate();
|
|
var owner = CreateOwner(kind);
|
|
SetOverflowPopupTemplate(owner, template);
|
|
var host = new ScopeAwareOverlayLayerPanel { Width = 420, Height = 320 };
|
|
host.Children.Add(owner);
|
|
var window = new AvaloniaWindow
|
|
{
|
|
Width = 420,
|
|
Height = 320,
|
|
Content = CreatePopupOverlayHost(host)
|
|
};
|
|
|
|
window.Show();
|
|
Dispatcher.UIThread.RunJobs();
|
|
|
|
var viewer = OpenOverflow(owner);
|
|
var context = viewer.OverflowPopupContext.ShouldNotBeNull();
|
|
var root = viewer.OverflowPopupRoot.ShouldNotBeNull();
|
|
var references = new OverflowGraphReferences(
|
|
new WeakReference(owner),
|
|
new WeakReference(viewer),
|
|
new WeakReference(context),
|
|
new WeakReference(root));
|
|
|
|
host.Children.Remove(owner);
|
|
Dispatcher.UIThread.RunJobs();
|
|
window.Content = null;
|
|
window.Close();
|
|
Dispatcher.UIThread.RunJobs();
|
|
return references;
|
|
}
|
|
|
|
private static void CollectGarbage()
|
|
{
|
|
for (var iteration = 0; iteration < 3; iteration++)
|
|
{
|
|
GC.Collect(GC.MaxGeneration, GCCollectionMode.Forced, blocking: true, compacting: true);
|
|
GC.WaitForPendingFinalizers();
|
|
}
|
|
}
|
|
|
|
private static VisualLayerManager CreatePopupOverlayHost(Control content)
|
|
{
|
|
var visualLayerManager = new VisualLayerManager { Child = content };
|
|
var property = typeof(VisualLayerManager).GetProperty(
|
|
"EnablePopupOverlayLayer",
|
|
BindingFlags.Instance | BindingFlags.NonPublic);
|
|
property.ShouldNotBeNull();
|
|
property.SetValue(visualLayerManager, true);
|
|
return visualLayerManager;
|
|
}
|
|
|
|
public enum OwnerKind
|
|
{
|
|
TabControl,
|
|
CardTabControl,
|
|
TabStrip,
|
|
CardTabStrip
|
|
}
|
|
|
|
private sealed record ContextTargetReleaseResult(
|
|
TabOverflowPopupContext Context,
|
|
WeakReference Target,
|
|
WeakReference OwnerGraph);
|
|
|
|
private sealed record OverflowGraphReferences(
|
|
WeakReference Owner,
|
|
WeakReference Viewer,
|
|
WeakReference Context,
|
|
WeakReference PopupRoot);
|
|
|
|
private sealed class RetainingActionTarget(object ownerGraph) : ITabOverflowPopupActionTarget
|
|
{
|
|
private readonly object _ownerGraph = ownerGraph;
|
|
|
|
public bool TryActivate(long sessionId, TabOverflowItem item) => false;
|
|
|
|
public bool TryClose(long sessionId, TabOverflowItem item) => false;
|
|
|
|
public void Dismiss(long sessionId)
|
|
{
|
|
GC.KeepAlive(_ownerGraph);
|
|
}
|
|
}
|
|
|
|
private sealed class CapturingTemplate : IDataTemplate
|
|
{
|
|
public int BuildCount { get; private set; }
|
|
public Control? Root { get; private set; }
|
|
|
|
public bool Match(object? data) => data is TabOverflowPopupContext;
|
|
|
|
public Control Build(object? param)
|
|
{
|
|
param.ShouldBeOfType<TabOverflowPopupContext>();
|
|
BuildCount++;
|
|
return Root = new Border { Name = $"OverflowRoot{BuildCount}" };
|
|
}
|
|
}
|
|
}
|