diff --git a/src/AtomUI.Desktop.Controls/Window/Window.cs b/src/AtomUI.Desktop.Controls/Window/Window.cs index 8e92a7788..0f4466fbe 100644 --- a/src/AtomUI.Desktop.Controls/Window/Window.cs +++ b/src/AtomUI.Desktop.Controls/Window/Window.cs @@ -360,6 +360,7 @@ public class Window : AvaloniaWindow, this.AttachClickThroughShadow( s_clickThroughShadowExtraAffectsProperties, () => FrameShadowThickness); + ScalingChanged += HandleLinuxScalingChanged; } } @@ -386,7 +387,7 @@ public class Window : AvaloniaWindow, _isLinuxInitialShowStatePrepared = true; EnsureInitialized(); ApplyStyling(); - EnsureMinSizeForDecorations(); + ApplyLinuxX11CsdFrameExtents(); var screen = Screens.ScreenFromPoint(Position) ?? Screens.Primary; var clientSize = SynchronizeLinuxInitialClientSize(screen); if (clientSize is null) @@ -823,7 +824,7 @@ public class Window : AvaloniaWindow, protected override void OnOpened(EventArgs e) { base.OnOpened(e); - EnsureMinSizeForDecorations(); + ApplyLinuxX11CsdFrameExtents(); ApplyDefaultLogoIfNeeded(); if (OperatingSystem.IsMacOS()) { @@ -948,11 +949,12 @@ public class Window : AvaloniaWindow, } } } - if (change.Property == WindowStateProperty || - change.Property == FrameShadowThicknessProperty || - change.Property == CornerRadiusProperty) + if (OperatingSystem.IsLinux() && + (change.Property == WindowStateProperty || + change.Property == FrameShadowThicknessProperty || + change.Property == IsCsdEnabledProperty)) { - EnsureMinSizeForDecorations(); + ApplyLinuxX11CsdFrameExtents(); } if (OperatingSystem.IsWindows() && change.Property == WindowStateProperty) { @@ -1024,6 +1026,22 @@ public class Window : AvaloniaWindow, } } + private void HandleLinuxScalingChanged(object? sender, EventArgs e) + { + ApplyLinuxX11CsdFrameExtents(); + } + + private void ApplyLinuxX11CsdFrameExtents() + { + if (!OperatingSystem.IsLinux()) + { + return; + } + + var frameExtents = WindowState is WindowState.Normal ? FrameShadowThickness : default; + this.SetLinuxX11CsdFrameExtents(frameExtents); + } + private void ConfigureCustomResizerVisible() { if (OsType != OsType.Linux || IsCsdEnabled) @@ -1035,47 +1053,4 @@ public class Window : AvaloniaWindow, IsCustomResizerVisible = CanResize && WindowState == WindowState.Normal; } } - - private void EnsureMinSizeForDecorations() - { - if (!OperatingSystem.IsLinux()) - { - return; - } - - if (WindowState is WindowState.Maximized or WindowState.FullScreen) - { - return; - } - - var shadow = FrameShadowThickness; - var cornerRadius = CornerRadius; - var maxCorner = Math.Max( - Math.Max(cornerRadius.TopLeft, cornerRadius.TopRight), - Math.Max(cornerRadius.BottomLeft, cornerRadius.BottomRight)); - - const double frameBorder = 1; - - var horizontalDecoration = shadow.Left + shadow.Right + frameBorder * 2; - var titleBarWidth = _titleBar?.DesiredSize.Width ?? 0; - var minDecorationWidth = Math.Max( - horizontalDecoration + maxCorner * 2, - horizontalDecoration + titleBarWidth); - - var verticalDecoration = shadow.Top + shadow.Bottom + frameBorder * 2; - var titleBarActualHeight = _titleBar?.DesiredSize.Height ?? TitleBarHeight; - var minDecorationHeight = verticalDecoration - + titleBarActualHeight - + maxCorner * 2; - - if (MinWidth < minDecorationWidth) - { - MinWidth = minDecorationWidth; - } - - if (MinHeight < minDecorationHeight) - { - MinHeight = minDecorationHeight; - } - } } diff --git a/src/AtomUI.Native/Linux/WindowUtils.Interop.cs b/src/AtomUI.Native/Linux/WindowUtils.Interop.cs index 2ac2ea854..33a37cd47 100644 --- a/src/AtomUI.Native/Linux/WindowUtils.Interop.cs +++ b/src/AtomUI.Native/Linux/WindowUtils.Interop.cs @@ -30,6 +30,9 @@ internal static class WindowUtilsInterop // Atom 常量 internal const uint XCB_ATOM_CARDINAL = 6; + // XChangeProperty mode + internal const int PropModeReplace = 0; + // 枚举类型(对应C中的typedef enum) internal enum xcb_shape_op_t : byte { @@ -171,6 +174,20 @@ internal static class WindowUtilsInterop [DllImport("libX11.so.6", EntryPoint = "XSetWMNormalHints")] internal static extern int XSetWMNormalHints(IntPtr display, IntPtr window, ref XSizeHints hints); + [DllImport("libX11.so.6", EntryPoint = "XInternAtom", CharSet = CharSet.Ansi)] + internal static extern IntPtr XInternAtom(IntPtr display, string atomName, bool onlyIfExists); + + [DllImport("libX11.so.6", EntryPoint = "XChangeProperty")] + internal static extern int XChangeProperty( + IntPtr display, + IntPtr window, + IntPtr property, + IntPtr type, + int format, + int mode, + IntPtr[] data, + int elementCount); + #endregion #region 窗口几何函数 diff --git a/src/AtomUI.Native/Linux/WindowUtils.Linux.cs b/src/AtomUI.Native/Linux/WindowUtils.Linux.cs index 94547ecc6..622eee9de 100644 --- a/src/AtomUI.Native/Linux/WindowUtils.Linux.cs +++ b/src/AtomUI.Native/Linux/WindowUtils.Linux.cs @@ -8,6 +8,8 @@ namespace AtomUI.Native; [SupportedOSPlatform("linux")] internal static class WindowUtilsLinux { + private const string X11CsdFrameExtentsPropertyName = "_GTK_FRAME_EXTENTS"; + public static void SetWindowIgnoreMouseEventsLinux(IntPtr handle, bool flag) { if (handle == IntPtr.Zero) @@ -228,6 +230,66 @@ internal static class WindowUtilsLinux } } + public static void SetX11CsdFrameExtents( + IntPtr handle, + int left, + int top, + int right, + int bottom) + { + if (handle == IntPtr.Zero) + { + return; + } + + var display = WindowUtilsInterop.XOpenDisplay(IntPtr.Zero); + if (display == IntPtr.Zero) + { + return; + } + + try + { + var property = WindowUtilsInterop.XInternAtom(display, X11CsdFrameExtentsPropertyName, false); + if (property == IntPtr.Zero) + { + return; + } + + // _GTK_FRAME_EXTENTS is the historical X11 atom used by Mutter and KWin + // to account for client-side decoration extents during snapping/placement. + // The property order is left, right, top, bottom and values are device pixels. + // Xlib expects format=32 data as C long[], so use IntPtr-sized elements. + var extents = new[] + { + ToCardinal(left), + ToCardinal(right), + ToCardinal(top), + ToCardinal(bottom) + }; + + WindowUtilsInterop.XChangeProperty( + display, + handle, + property, + new IntPtr((int)WindowUtilsInterop.XCB_ATOM_CARDINAL), + 32, + WindowUtilsInterop.PropModeReplace, + extents, + extents.Length); + WindowUtilsInterop.XFlush(display); + } + finally + { + WindowUtilsInterop.XCloseDisplay(display); + } + } + + private static IntPtr ToCardinal(int value) + { + return new IntPtr(Math.Max(0, value)); + } + /// /// 获取 Linux 系统默认的标题栏高度。 /// 使用 Avalonia 的 WindowDecorationMargin 属性获取标题栏高度。 diff --git a/src/AtomUI.Native/WindowExtensions.cs b/src/AtomUI.Native/WindowExtensions.cs index 32f72484f..3f38100d4 100644 --- a/src/AtomUI.Native/WindowExtensions.cs +++ b/src/AtomUI.Native/WindowExtensions.cs @@ -201,6 +201,29 @@ internal static class WindowExtensions maxHeight); } + [SupportedOSPlatform("linux")] + public static void SetLinuxX11CsdFrameExtents(this Window window, Thickness frameExtents) + { + if (!OperatingSystem.IsLinux()) + { + return; + } + + var handle = window.TryGetPlatformHandle(); + if (handle is null || handle.HandleDescriptor != "XID") + { + return; + } + + var scaling = window.RenderScaling <= 0 ? 1.0 : window.RenderScaling; + WindowUtilsLinux.SetX11CsdFrameExtents( + handle.Handle, + ToPixelMargin(frameExtents.Left, scaling), + ToPixelMargin(frameExtents.Top, scaling), + ToPixelMargin(frameExtents.Right, scaling), + ToPixelMargin(frameExtents.Bottom, scaling)); + } + private static int ToPixelLength(double value, double scaling) { if (!double.IsFinite(value) || value <= 0) @@ -218,4 +241,13 @@ internal static class WindowExtensions } return ToPixelLength(value, scaling); } + + private static int ToPixelMargin(double value, double scaling) + { + if (!double.IsFinite(value) || value <= 0) + { + return 0; + } + return Math.Max(0, (int)Math.Ceiling(value * scaling)); + } } diff --git a/tests/AtomUI.Desktop.Controls.Tests/Window/WindowResizeArtifactTests.cs b/tests/AtomUI.Desktop.Controls.Tests/Window/WindowResizeArtifactTests.cs index a1f5bce7d..d304d318f 100644 --- a/tests/AtomUI.Desktop.Controls.Tests/Window/WindowResizeArtifactTests.cs +++ b/tests/AtomUI.Desktop.Controls.Tests/Window/WindowResizeArtifactTests.cs @@ -52,6 +52,52 @@ public class WindowResizeArtifactTests interopSource.ShouldContain("PSize"); } + [Fact] + public void Linux_Client_Drawn_Shadow_Publishes_X11_Csd_Frame_Extents() + { + var windowSource = File.ReadAllText(GetRepoFile("src/AtomUI.Desktop.Controls/Window/Window.cs")); + var extensionSource = File.ReadAllText(GetRepoFile("src/AtomUI.Native/WindowExtensions.cs")); + var linuxSource = File.ReadAllText(GetRepoFile("src/AtomUI.Native/Linux/WindowUtils.Linux.cs")); + var interopSource = File.ReadAllText(GetRepoFile("src/AtomUI.Native/Linux/WindowUtils.Interop.cs")); + + windowSource.ShouldContain("ScalingChanged += HandleLinuxScalingChanged;"); + windowSource.ShouldContain("ApplyLinuxX11CsdFrameExtents();"); + windowSource.ShouldContain("WindowState is WindowState.Normal ? FrameShadowThickness : default"); + windowSource.ShouldContain("this.SetLinuxX11CsdFrameExtents(frameExtents);"); + windowSource.ShouldNotContain("EnsureMinSizeForDecorations"); + windowSource.IndexOf("PrepareLinuxInitialShowState();", StringComparison.Ordinal) + .ShouldBeLessThan(windowSource.IndexOf("base.Show();", StringComparison.Ordinal)); + + extensionSource.ShouldContain("SetLinuxX11CsdFrameExtents"); + extensionSource.ShouldContain("handle.HandleDescriptor != \"XID\""); + extensionSource.ShouldContain("ToPixelMargin(frameExtents.Left, scaling)"); + extensionSource.ShouldContain("ToPixelMargin(frameExtents.Top, scaling)"); + extensionSource.ShouldContain("ToPixelMargin(frameExtents.Right, scaling)"); + extensionSource.ShouldContain("ToPixelMargin(frameExtents.Bottom, scaling)"); + + linuxSource.ShouldContain("X11CsdFrameExtentsPropertyName = \"_GTK_FRAME_EXTENTS\""); + linuxSource.ShouldContain("SetX11CsdFrameExtents"); + linuxSource.ShouldContain("Mutter and KWin"); + linuxSource.ShouldContain("left, right, top, bottom"); + linuxSource.ShouldContain("XInternAtom"); + linuxSource.ShouldContain("XChangeProperty"); + linuxSource.IndexOf("ToCardinal(left)", StringComparison.Ordinal) + .ShouldBeLessThan(linuxSource.IndexOf("ToCardinal(right)", StringComparison.Ordinal)); + linuxSource.IndexOf("ToCardinal(right)", StringComparison.Ordinal) + .ShouldBeLessThan(linuxSource.IndexOf("ToCardinal(top)", StringComparison.Ordinal)); + linuxSource.IndexOf("ToCardinal(top)", StringComparison.Ordinal) + .ShouldBeLessThan(linuxSource.IndexOf("ToCardinal(bottom)", StringComparison.Ordinal)); + + interopSource.ShouldContain("PropModeReplace"); + interopSource.ShouldContain("XInternAtom"); + interopSource.ShouldContain("XChangeProperty"); + interopSource.ShouldContain("IntPtr[] data"); + + windowSource.ShouldNotContain("GtkFrameExtents"); + extensionSource.ShouldNotContain("GtkFrameExtents"); + linuxSource.ShouldNotContain("GtkFrameExtents"); + } + [Fact] public void Linux_NonCsd_Window_Template_Clips_Content_To_Window_CornerRadius() {