fix(Window): improve Linux window scaling handling and frame extents configuration

This commit is contained in:
chinboy
2026-06-15 23:55:00 +08:00
parent a47e419170
commit 479abcf161
5 changed files with 181 additions and 49 deletions
+24 -49
View File
@@ -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;
}
}
}
@@ -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 窗口几何函数
@@ -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));
}
/// <summary>
/// 获取 Linux 系统默认的标题栏高度。
/// 使用 Avalonia 的 WindowDecorationMargin 属性获取标题栏高度。
+32
View File
@@ -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));
}
}
@@ -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()
{