mirror of
https://github.com/AtomUI/AtomUI.git
synced 2026-10-28 23:08:36 +08:00
fix(Window): improve Linux window scaling handling and frame extents configuration
This commit is contained in:
@@ -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 属性获取标题栏高度。
|
||||
|
||||
@@ -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()
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user