diff --git a/api/Avalonia.nupkg.xml b/api/Avalonia.nupkg.xml
index ab26c5e135..cf7ef0deca 100644
--- a/api/Avalonia.nupkg.xml
+++ b/api/Avalonia.nupkg.xml
@@ -1,4 +1,4 @@
-
+
@@ -169,6 +169,12 @@
baseline/Avalonia/lib/net10.0/Avalonia.Controls.dll
current/Avalonia/lib/net10.0/Avalonia.Controls.dll
+
+ CP0006
+ M:Avalonia.Platform.IPopupImpl.SetHitTestVisible(System.Boolean)
+ baseline/Avalonia/lib/net10.0/Avalonia.Controls.dll
+ current/Avalonia/lib/net10.0/Avalonia.Controls.dll
+
CP0006
M:Avalonia.Platform.ITopLevelImpl.SetFrameThemeVariant(System.Nullable{Avalonia.Platform.PlatformThemeVariant})
@@ -199,10 +205,16 @@
baseline/Avalonia/lib/net8.0/Avalonia.Controls.dll
current/Avalonia/lib/net8.0/Avalonia.Controls.dll
+
+ CP0006
+ M:Avalonia.Platform.IPopupImpl.SetHitTestVisible(System.Boolean)
+ baseline/Avalonia/lib/net8.0/Avalonia.Controls.dll
+ current/Avalonia/lib/net8.0/Avalonia.Controls.dll
+
CP0006
M:Avalonia.Platform.ITopLevelImpl.SetFrameThemeVariant(System.Nullable{Avalonia.Platform.PlatformThemeVariant})
baseline/Avalonia/lib/net8.0/Avalonia.Controls.dll
current/Avalonia/lib/net8.0/Avalonia.Controls.dll
-
+
\ No newline at end of file
diff --git a/native/Avalonia.Native/src/OSX/PopupImpl.mm b/native/Avalonia.Native/src/OSX/PopupImpl.mm
index 13131bf91e..8a188c89b3 100644
--- a/native/Avalonia.Native/src/OSX/PopupImpl.mm
+++ b/native/Avalonia.Native/src/OSX/PopupImpl.mm
@@ -43,6 +43,17 @@ public:
return WindowBaseImpl::Show(activate, true);
}
+ virtual HRESULT SetHitTestVisible(bool value) override
+ {
+ START_COM_CALL;
+
+ @autoreleasepool
+ {
+ [Window setIgnoresMouseEvents:!value];
+ return S_OK;
+ }
+ }
+
virtual bool ShouldTakeFocusOnShow() override
{
auto parent = Parent.tryGet();
diff --git a/src/Avalonia.Controls/Platform/IPopupImpl.cs b/src/Avalonia.Controls/Platform/IPopupImpl.cs
index 388c447243..58bb164426 100644
--- a/src/Avalonia.Controls/Platform/IPopupImpl.cs
+++ b/src/Avalonia.Controls/Platform/IPopupImpl.cs
@@ -13,5 +13,12 @@ namespace Avalonia.Platform
void SetWindowManagerAddShadowHint(bool enabled);
void TakeFocus();
+
+ ///
+ /// Sets whether the popup window takes part in pointer hit testing. When false, the
+ /// native window is made input-transparent so that pointer input passes through to
+ /// whatever is behind it.
+ ///
+ void SetHitTestVisible(bool isHitTestVisible);
}
}
diff --git a/src/Avalonia.Controls/Primitives/IPopupHost.cs b/src/Avalonia.Controls/Primitives/IPopupHost.cs
index 3e9967f235..df6b325558 100644
--- a/src/Avalonia.Controls/Primitives/IPopupHost.cs
+++ b/src/Avalonia.Controls/Primitives/IPopupHost.cs
@@ -57,6 +57,11 @@ namespace Avalonia.Controls.Primitives
///
bool Topmost { get; set; }
+ ///
+ /// Gets or sets whether the popup takes part in pointer hit testing.
+ ///
+ bool IsHitTestVisible { get; set; }
+
///
/// Gets or sets a transform that will be applied to the popup.
///
diff --git a/src/Avalonia.Controls/Primitives/Popup.cs b/src/Avalonia.Controls/Primitives/Popup.cs
index 86d4fb746e..75dd723fe3 100644
--- a/src/Avalonia.Controls/Primitives/Popup.cs
+++ b/src/Avalonia.Controls/Primitives/Popup.cs
@@ -159,7 +159,6 @@ namespace Avalonia.Controls.Primitives
///
static Popup()
{
- IsHitTestVisibleProperty.OverrideDefaultValue(false);
ChildProperty.Changed.AddClassHandler((x, e) => x.ChildChanged(e));
IsOpenProperty.Changed.AddClassHandler((x, e) => x.IsOpenChanged((AvaloniaPropertyChangedEventArgs)e));
}
@@ -451,6 +450,7 @@ namespace Avalonia.Controls.Primitives
UpdateHostSizing(popupHost, topLevel, placementTarget);
popupHost.Topmost = Topmost;
+ popupHost.IsHitTestVisible = IsHitTestVisible;
popupHost.SetChild(Child);
((ISetLogicalParent)popupHost).SetParent(this);
@@ -691,6 +691,10 @@ namespace Avalonia.Controls.Primitives
{
_openState.PopupHost.Topmost = change.GetNewValue();
}
+ else if (change.Property == IsHitTestVisibleProperty)
+ {
+ _openState.PopupHost.IsHitTestVisible = change.GetNewValue();
+ }
}
}
diff --git a/src/Avalonia.Controls/Primitives/PopupRoot.cs b/src/Avalonia.Controls/Primitives/PopupRoot.cs
index d1fd1317f8..93a30b06a3 100644
--- a/src/Avalonia.Controls/Primitives/PopupRoot.cs
+++ b/src/Avalonia.Controls/Primitives/PopupRoot.cs
@@ -62,6 +62,7 @@ namespace Avalonia.Controls.Primitives
{
ParentTopLevel = parent;
impl.SetWindowManagerAddShadowHint(WindowManagerAddShadowHint);
+ impl.SetHitTestVisible(IsHitTestVisible);
}
///
@@ -219,6 +220,10 @@ namespace Avalonia.Controls.Primitives
{
PlatformImpl?.SetTopmost(change.GetNewValue());
}
+ else if (change.Property == IsHitTestVisibleProperty)
+ {
+ PlatformImpl?.SetHitTestVisible(change.GetNewValue());
+ }
}
}
}
diff --git a/src/Avalonia.DesignerSupport/Remote/Stubs.cs b/src/Avalonia.DesignerSupport/Remote/Stubs.cs
index 97fda7d8d1..6843911df4 100644
--- a/src/Avalonia.DesignerSupport/Remote/Stubs.cs
+++ b/src/Avalonia.DesignerSupport/Remote/Stubs.cs
@@ -215,6 +215,8 @@ namespace Avalonia.DesignerSupport.Remote
return null;
}
public void TakeFocus() { }
+
+ public void SetHitTestVisible(bool isHitTestVisible) { }
}
class ClipboardStub : IClipboard
diff --git a/src/Avalonia.Native/PopupImpl.cs b/src/Avalonia.Native/PopupImpl.cs
index 222fa63497..ffee48d3ad 100644
--- a/src/Avalonia.Native/PopupImpl.cs
+++ b/src/Avalonia.Native/PopupImpl.cs
@@ -8,6 +8,7 @@ namespace Avalonia.Native
class PopupImpl : WindowBaseImpl, IPopupImpl
{
private readonly ITopLevelImpl _parent;
+ private readonly IAvnPopup _native;
public PopupImpl(IAvaloniaNativeFactory factory,
ITopLevelImpl parent) : base(factory)
@@ -16,7 +17,7 @@ namespace Avalonia.Native
using (var e = new PopupEvents(this))
{
- Init(new MacOSTopLevelHandle(factory.CreatePopup(e)));
+ Init(new MacOSTopLevelHandle(_native = factory.CreatePopup(e)));
}
PopupPositioner = new ManagedPopupPositioner(new ManagedPopupPositionerPopupImplHelper(parent, MoveResize));
@@ -87,6 +88,11 @@ namespace Avalonia.Native
{
}
+ public void SetHitTestVisible(bool isHitTestVisible)
+ {
+ _native.SetHitTestVisible(isHitTestVisible.AsComBool());
+ }
+
public void TakeFocus()
{
var parent = _parent;
diff --git a/src/Avalonia.Native/avn.idl b/src/Avalonia.Native/avn.idl
index 32d3f4286e..91a252c020 100644
--- a/src/Avalonia.Native/avn.idl
+++ b/src/Avalonia.Native/avn.idl
@@ -791,7 +791,7 @@ interface IAvnWindowBase : IAvnTopLevel
[uuid(83e588f3-6981-4e48-9ea0-e1e569f79a91), cpp-virtual-inherits]
interface IAvnPopup : IAvnWindowBase
{
-
+ HRESULT SetHitTestVisible(bool value);
}
[uuid(cab661de-49d6-4ead-b59c-eac9b2b6c28d), cpp-virtual-inherits]
diff --git a/src/Avalonia.Wayland/PopupImpl.Sink.cs b/src/Avalonia.Wayland/PopupImpl.Sink.cs
index 8576f2ba60..2e1e8eb746 100644
--- a/src/Avalonia.Wayland/PopupImpl.Sink.cs
+++ b/src/Avalonia.Wayland/PopupImpl.Sink.cs
@@ -43,6 +43,10 @@ partial class PopupImpl
// Re-apply cursor (defaults to Arrow on a fresh worker WSurface).
if (Parent.CurrentCursor is not null)
Parent.ApplyCurrentCursor(_surfaceProxy);
+
+ // A fresh worker WSurface starts hit-test visible.
+ if (!Parent._isHitTestVisible)
+ _surfaceProxy.SetHitTestVisible(false);
}
protected override void DisconnectFromSurface()
diff --git a/src/Avalonia.Wayland/PopupImpl.cs b/src/Avalonia.Wayland/PopupImpl.cs
index 0edd318f1b..e0cfb75ddc 100644
--- a/src/Avalonia.Wayland/PopupImpl.cs
+++ b/src/Avalonia.Wayland/PopupImpl.cs
@@ -33,6 +33,7 @@ internal partial class PopupImpl : WindowBaseImpl, IPopupImpl
private WaylandSurfaceCreateResult? _handle;
private WXdgPopupProxy? _surfaceProxy;
private XdgPopupPositionerParams? _lastPositioner;
+ private bool _isHitTestVisible = true;
public PopupImpl(WaylandWorkerClient client, WindowBaseImpl parent) : base(client)
{
@@ -108,6 +109,12 @@ internal partial class PopupImpl : WindowBaseImpl, IPopupImpl
{
}
+ public void SetHitTestVisible(bool isHitTestVisible)
+ {
+ _isHitTestVisible = isHitTestVisible;
+ _surfaceProxy?.SetHitTestVisible(isHitTestVisible);
+ }
+
///
/// Bridges Avalonia's positioner contract to .
///
diff --git a/src/Avalonia.Wayland/Server/Persistent/IWSurface.cs b/src/Avalonia.Wayland/Server/Persistent/IWSurface.cs
index 94a6d1b009..8783db836e 100644
--- a/src/Avalonia.Wayland/Server/Persistent/IWSurface.cs
+++ b/src/Avalonia.Wayland/Server/Persistent/IWSurface.cs
@@ -58,4 +58,12 @@ internal interface IWSurface
/// Resets the IME state (clears any pending preedit/commit).
void ResetTextInput();
+
+ ///
+ /// Sets whether the surface takes part in pointer/touch hit testing. When
+ /// false an empty wl_region is installed as the input region, so the
+ /// compositor routes input to whatever is behind. Cached and re-applied on
+ /// reconnect.
+ ///
+ void SetHitTestVisible(bool value);
}
diff --git a/src/Avalonia.Wayland/Server/Persistent/WSurface.cs b/src/Avalonia.Wayland/Server/Persistent/WSurface.cs
index 0fd1745864..1db7515b74 100644
--- a/src/Avalonia.Wayland/Server/Persistent/WSurface.cs
+++ b/src/Avalonia.Wayland/Server/Persistent/WSurface.cs
@@ -29,6 +29,7 @@ class WSurface : IPersistentWaylandObject, IWSurface, IWaylandFramebufferSurface
protected double? PreferredFractionalScale { get; private set; }
protected List Outputs { get; } = new();
private WlCallback? _frameCallback;
+ private bool _hitTestVisible = true;
private double _currentScale = 1;
private const double ScaleEpsilon = 1e-6;
private readonly List _activeRenderTargets = new();
@@ -115,6 +116,65 @@ class WSurface : IPersistentWaylandObject, IWSurface, IWaylandFramebufferSurface
public virtual void ResetTextInput() => TextInputV3?.Reset(this);
+ public void SetHitTestVisible(bool value)
+ {
+ if (_hitTestVisible == value)
+ return;
+ _hitTestVisible = value;
+ // No-op while disconnected; OnConnected replays the cached value.
+ if (WlSurface == null)
+ return;
+ ApplyInputRegion();
+ // Toggling hit-test visibility changes nothing about what's drawn, so an
+ // idle surface may never attach another buffer. Commit out of band to
+ // promote the region — a commit without an attach is well-defined.
+ if (CanCommitOutOfBand)
+ WlSurface.Commit();
+ }
+
+ ///
+ /// Whether committing this surface outside of a buffer attach is legal right
+ /// now. When it isn't, staged double-buffered state stays pending and is
+ /// promoted by the surface's own next commit.
+ ///
+ ///
+ /// Such a commit can't promote another frame's half-staged state: UI→worker
+ /// proxy calls land as compositor server jobs, which ServerCompositor.RenderCore
+ /// drains before it renders any target, while the per-frame staging
+ /// () and the attach+commit that
+ /// promotes it are one synchronous block inside the render pass.
+ ///
+ protected virtual bool CanCommitOutOfBand => true;
+
+ ///
+ /// Installs the input region matching the cached hit-test state. An empty
+ /// region makes the compositor route pointer/touch input to whatever is
+ /// behind this surface.
+ ///
+ private void ApplyInputRegion()
+ {
+ if (WlSurface == null)
+ return;
+
+ if (_hitTestVisible)
+ {
+ // null is the protocol default: an infinite input region.
+ WlSurface.SetInputRegion(null!);
+ return;
+ }
+
+ var region = Globals!.WlCompositor.CreateRegion();
+ try
+ {
+ WlSurface.SetInputRegion(region);
+ }
+ finally
+ {
+ region.Destroy();
+ region.Dispose();
+ }
+ }
+
public virtual void OnConnected(WaylandConnection connection, WaylandGlobals globals)
{
Connection = connection;
@@ -127,6 +187,11 @@ class WSurface : IPersistentWaylandObject, IWSurface, IWaylandFramebufferSurface
WlSurface, new FractionalScaleListener(this), connection.Queue);
Viewport = globals.Viewporter!.GetViewport(WlSurface);
}
+
+ // Re-apply the cached input region on (re)connect. It's double-buffered
+ // state, promoted by the next commit — which happens before the surface
+ // can receive any input.
+ ApplyInputRegion();
}
private IPlatformRenderSurface[]? _renderSurfaces;
@@ -346,6 +411,12 @@ class WXdgShellSurface : WSurface, IWXdgShellSurface
/// True iff this surface is currently mapped (xdg-shell sense).
internal bool IsMapped => _mapped;
+ // Committing an xdg_surface before it has a role, or before its initial
+ // configure has been acked, is a protocol error. Until we're mapped, staged
+ // state rides along on the commit that assigns the role (TryAttachToParent)
+ // or on the first buffer attach.
+ protected override bool CanCommitOutOfBand => _mapped;
+
internal void RegisterPendingChildPopup(WXdgPopup popup) => _pendingChildPopups.Add(popup);
internal void UnregisterPendingChildPopup(WXdgPopup popup) => _pendingChildPopups.Remove(popup);
diff --git a/src/Avalonia.X11/X11Enums.cs b/src/Avalonia.X11/X11Enums.cs
index 9d671ae6d2..3325984966 100644
--- a/src/Avalonia.X11/X11Enums.cs
+++ b/src/Avalonia.X11/X11Enums.cs
@@ -107,4 +107,15 @@ namespace Avalonia.X11
CWColormap = (1 << 13),
CWCursor = (1 << 14),
}
+
+ ///
+ /// Shape kinds from the X Nonrectangular Window Shape extension, as accepted by
+ /// XFixesSetWindowShapeRegion.
+ ///
+ internal enum ShapeKind
+ {
+ ShapeBounding = 0,
+ ShapeClip = 1,
+ ShapeInput = 2,
+ }
}
diff --git a/src/Avalonia.X11/X11Info.cs b/src/Avalonia.X11/X11Info.cs
index cc4ec67a3a..67abadc0e3 100644
--- a/src/Avalonia.X11/X11Info.cs
+++ b/src/Avalonia.X11/X11Info.cs
@@ -32,6 +32,11 @@ namespace Avalonia.X11
public bool HasXim { get; }
public bool HasXSync { get; }
+ ///
+ /// Whether XFixes is usable, which is what window input shapes are set through.
+ ///
+ public bool HasXFixes { get; }
+
public IntPtr DefaultFontSet { get; }
public bool HasXkb { get; }
@@ -126,6 +131,18 @@ namespace Avalonia.X11
//Ignore, XSync is not supported
}
+ try
+ {
+ // Input shapes need XFixes 2.0 or newer.
+ HasXFixes = XFixesQueryExtension(display, out _, out _) != 0
+ && XFixesQueryVersion(display, out var fixesMajor, out _) != 0
+ && fixesMajor >= 2;
+ }
+ catch
+ {
+ //Ignore, XFixes is not supported
+ }
+
try
{
var xkbMajor = 1;
diff --git a/src/Avalonia.X11/X11Window.cs b/src/Avalonia.X11/X11Window.cs
index 1ed61248d0..5337e99a4e 100644
--- a/src/Avalonia.X11/X11Window.cs
+++ b/src/Avalonia.X11/X11Window.cs
@@ -1679,6 +1679,34 @@ namespace Avalonia.X11
{
}
+ public void SetHitTestVisible(bool isHitTestVisible)
+ {
+ if (!_x11.HasXFixes)
+ return;
+
+ // An empty input region makes the server route pointer input to whatever is behind
+ // the window. None (0) restores the default input shape, i.e. the whole window.
+ var region = IntPtr.Zero;
+ if (!isHitTestVisible)
+ {
+ var rect = default(XRectangle);
+ region = XFixesCreateRegion(_x11.Display, &rect, 0);
+ }
+
+ XFixesSetWindowShapeRegion(_x11.Display, _handle, ShapeKind.ShapeInput, 0, 0, region);
+
+ // The render window is a child of _handle when a GPU backend is in use. Shaping the
+ // parent alone doesn't take the child out of the input hierarchy, so it would keep
+ // capturing the pointer.
+ if (_renderHandle != _handle)
+ XFixesSetWindowShapeRegion(_x11.Display, _renderHandle, ShapeKind.ShapeInput, 0, 0, region);
+
+ if (region != IntPtr.Zero)
+ XFixesDestroyRegion(_x11.Display, region);
+
+ XFlush(_x11.Display);
+ }
+
public WindowTransparencyLevel TransparencyLevel =>
_transparencyHelper?.CurrentLevel ?? WindowTransparencyLevel.None;
diff --git a/src/Avalonia.X11/XLib.cs b/src/Avalonia.X11/XLib.cs
index 108ae257c4..0e56bcea5e 100644
--- a/src/Avalonia.X11/XLib.cs
+++ b/src/Avalonia.X11/XLib.cs
@@ -21,6 +21,7 @@ namespace Avalonia.X11
private const string libX11Ext = "libXext.so.6";
private const string libXInput = "libXi.so.6";
private const string libXCursor = "libXcursor.so.1";
+ private const string libXFixes = "libXfixes.so.3";
public const IntPtr AnyPropertyType = 0;
@@ -610,6 +611,23 @@ namespace Avalonia.X11
[DllImport(libX11Ext)]
public static extern int XSyncSetCounter(IntPtr dpy, IntPtr counter, XSyncValue value);
+ [DllImport(libXFixes)]
+ public static extern int XFixesQueryExtension(IntPtr dpy, out int event_base_return, out int error_base_return);
+
+ [DllImport(libXFixes)]
+ public static extern int XFixesQueryVersion(IntPtr dpy, out int major_version_return,
+ out int minor_version_return);
+
+ [DllImport(libXFixes)]
+ public static extern IntPtr XFixesCreateRegion(IntPtr dpy, XRectangle* rectangles, int nrectangles);
+
+ [DllImport(libXFixes)]
+ public static extern void XFixesSetWindowShapeRegion(IntPtr dpy, IntPtr win, ShapeKind shape_kind,
+ int x_off, int y_off, IntPtr region);
+
+ [DllImport(libXFixes)]
+ public static extern void XFixesDestroyRegion(IntPtr dpy, IntPtr region);
+
[DllImport(libX11Randr)]
public static extern int XRRQueryVersion(IntPtr dpy,
out int major_version_return,
diff --git a/src/Headless/Avalonia.Headless/HeadlessWindowImpl.cs b/src/Headless/Avalonia.Headless/HeadlessWindowImpl.cs
index efc2c08a89..b3a6bb16cc 100644
--- a/src/Headless/Avalonia.Headless/HeadlessWindowImpl.cs
+++ b/src/Headless/Avalonia.Headless/HeadlessWindowImpl.cs
@@ -478,7 +478,11 @@ namespace Avalonia.Headless
}
- public void TakeFocus()
+ public void TakeFocus()
+ {
+ }
+
+ public void SetHitTestVisible(bool isHitTestVisible)
{
}
}
diff --git a/src/Windows/Avalonia.Win32/PopupImpl.cs b/src/Windows/Avalonia.Win32/PopupImpl.cs
index ee2feedffc..0e3a40f12a 100644
--- a/src/Windows/Avalonia.Win32/PopupImpl.cs
+++ b/src/Windows/Avalonia.Win32/PopupImpl.cs
@@ -13,6 +13,7 @@ namespace Avalonia.Win32
{
private readonly IWindowBaseImpl? _parent;
private bool _dropShadowHint = true;
+ private bool _isHitTestVisible = true;
private Size? _maxAutoSize;
@@ -87,6 +88,8 @@ namespace Avalonia.Win32
goto default;
case UnmanagedMethods.WindowsMessage.WM_MOUSEACTIVATE:
return (IntPtr)UnmanagedMethods.MouseActivate.MA_NOACTIVATE;
+ case UnmanagedMethods.WindowsMessage.WM_NCHITTEST when !_isHitTestVisible:
+ return (IntPtr)UnmanagedMethods.HitTestValues.HTTRANSPARENT;
default:
return base.WndProc(hWnd, msg, wParam, lParam);
}
@@ -152,6 +155,11 @@ namespace Avalonia.Win32
EnableBoxShadow(Handle.Handle, enabled);
}
+ public void SetHitTestVisible(bool isHitTestVisible)
+ {
+ _isHitTestVisible = isHitTestVisible;
+ }
+
public void TakeFocus()
{
var parent = _parent;
diff --git a/tests/Avalonia.Controls.UnitTests/Primitives/PopupRootTests.cs b/tests/Avalonia.Controls.UnitTests/Primitives/PopupRootTests.cs
index ee051649cd..c73c188627 100644
--- a/tests/Avalonia.Controls.UnitTests/Primitives/PopupRootTests.cs
+++ b/tests/Avalonia.Controls.UnitTests/Primitives/PopupRootTests.cs
@@ -42,6 +42,33 @@ namespace Avalonia.Controls.UnitTests.Primitives
}
}
+ [Fact]
+ public void PopupRoot_Forwards_Initial_IsHitTestVisible_To_Impl()
+ {
+ using (UnitTestApplication.Start(TestServices.StyledWindow))
+ {
+ var impl = MockWindowingPlatform.CreatePopupMock(new Mock().Object);
+
+ CreateTarget(new Window(), impl.Object);
+
+ impl.Verify(x => x.SetHitTestVisible(true));
+ }
+ }
+
+ [Fact]
+ public void PopupRoot_Forwards_IsHitTestVisible_Changes_To_Impl()
+ {
+ using (UnitTestApplication.Start(TestServices.StyledWindow))
+ {
+ var impl = MockWindowingPlatform.CreatePopupMock(new Mock().Object);
+ var target = CreateTarget(new Window(), impl.Object);
+
+ target.IsHitTestVisible = false;
+
+ impl.Verify(x => x.SetHitTestVisible(false));
+ }
+ }
+
[Fact]
public void PopupRoot_StylingParent_Is_Popup()
{
diff --git a/tests/Avalonia.Controls.UnitTests/Primitives/PopupTests.cs b/tests/Avalonia.Controls.UnitTests/Primitives/PopupTests.cs
index dcf9b6f653..3cca86d3b4 100644
--- a/tests/Avalonia.Controls.UnitTests/Primitives/PopupTests.cs
+++ b/tests/Avalonia.Controls.UnitTests/Primitives/PopupTests.cs
@@ -1318,6 +1318,48 @@ namespace Avalonia.Controls.UnitTests.Primitives
return result;
}
+ [Fact]
+ public void Popup_IsHitTestVisible_Defaults_To_True()
+ {
+ using (CreateServices())
+ {
+ Assert.True(new Popup().IsHitTestVisible);
+ }
+ }
+
+ [Fact]
+ public void Popup_Forwards_IsHitTestVisible_To_Host_On_Open()
+ {
+ using (CreateServices())
+ {
+ var target = new Popup { IsHitTestVisible = false };
+ var window = PreparedWindow(target);
+ window.Show();
+
+ target.Open();
+
+ Assert.False(target.Host!.IsHitTestVisible);
+ }
+ }
+
+ [Fact]
+ public void Popup_Forwards_IsHitTestVisible_Changes_To_Open_Host()
+ {
+ using (CreateServices())
+ {
+ var target = new Popup();
+ var window = PreparedWindow(target);
+ window.Show();
+
+ target.Open();
+ Assert.True(target.Host!.IsHitTestVisible);
+
+ target.IsHitTestVisible = false;
+
+ Assert.False(target.Host!.IsHitTestVisible);
+ }
+ }
+
[Fact]
public void Popup_Open_With_Correct_IsUsingOverlayLayer_And_Disabled_OverlayLayer()
{