diff --git a/api/Avalonia.nupkg.xml b/api/Avalonia.nupkg.xml
index 0e767b5ef3..71bdf3714d 100644
--- a/api/Avalonia.nupkg.xml
+++ b/api/Avalonia.nupkg.xml
@@ -2395,6 +2395,12 @@
baseline/Avalonia/lib/net10.0/Avalonia.Controls.dll
current/Avalonia/lib/net10.0/Avalonia.Controls.dll
+
+ CP0002
+ M:Avalonia.Controls.Window.ExtendClientAreaToDecorationsChanged(System.Boolean)
+ baseline/Avalonia/lib/net10.0/Avalonia.Controls.dll
+ current/Avalonia/lib/net10.0/Avalonia.Controls.dll
+
CP0002
M:Avalonia.Controls.Window.get_ExtendClientAreaChromeHints
@@ -4063,6 +4069,12 @@
baseline/Avalonia/lib/net8.0/Avalonia.Controls.dll
current/Avalonia/lib/net8.0/Avalonia.Controls.dll
+
+ CP0002
+ M:Avalonia.Controls.Window.ExtendClientAreaToDecorationsChanged(System.Boolean)
+ baseline/Avalonia/lib/net8.0/Avalonia.Controls.dll
+ current/Avalonia/lib/net8.0/Avalonia.Controls.dll
+
CP0002
M:Avalonia.Controls.Window.get_ExtendClientAreaChromeHints
diff --git a/src/Avalonia.Controls/Chrome/WindowDrawnDecorations.cs b/src/Avalonia.Controls/Chrome/WindowDrawnDecorations.cs
index 48847b5f59..7a282bf306 100644
--- a/src/Avalonia.Controls/Chrome/WindowDrawnDecorations.cs
+++ b/src/Avalonia.Controls/Chrome/WindowDrawnDecorations.cs
@@ -2,6 +2,7 @@ using System;
using Avalonia.Automation;
using Avalonia.Controls.Metadata;
using Avalonia.Controls.Primitives;
+using Avalonia.Layout;
using Avalonia.LogicalTree;
using Avalonia.Reactive;
using Avalonia.Styling;
@@ -138,6 +139,7 @@ public class WindowDrawnDecorations : StyledElement
private IDisposable? _windowSubscriptions;
private Window? _hostWindow;
private double _titleBarHeightOverride = -1;
+ private double _renderScaling = 1.0;
///
/// Raised when any property affecting the effective geometry changes
@@ -145,6 +147,22 @@ public class WindowDrawnDecorations : StyledElement
///
internal event Action? EffectiveGeometryChanged;
+ ///
+ /// Gets or sets the current render scaling factor used for pixel-aligning
+ /// decoration geometry (title bar height, frame/shadow thickness).
+ ///
+ internal double RenderScaling
+ {
+ get => _renderScaling;
+ set
+ {
+ if (_renderScaling == value)
+ return;
+ _renderScaling = value;
+ UpdateEffectiveGeometry();
+ }
+ }
+
///
/// Gets or sets the decorations template.
///
@@ -559,16 +577,19 @@ public class WindowDrawnDecorations : StyledElement
private void UpdateEffectiveGeometry()
{
+ var scale = _renderScaling;
+
TitleBarHeight = EnabledParts.HasFlag(DrawnWindowDecorationParts.TitleBar)
- ? (TitleBarHeightOverride == -1 ? DefaultTitleBarHeight : TitleBarHeightOverride)
+ ? LayoutHelper.RoundLayoutValue(
+ TitleBarHeightOverride == -1 ? DefaultTitleBarHeight : TitleBarHeightOverride, scale)
: 0;
FrameThickness = EnabledParts.HasFlag(DrawnWindowDecorationParts.Border)
- ? (FrameThicknessOverride ?? DefaultFrameThickness)
+ ? LayoutHelper.RoundLayoutThickness(FrameThicknessOverride ?? DefaultFrameThickness, scale)
: default;
ShadowThickness = EnabledParts.HasFlag(DrawnWindowDecorationParts.Shadow)
- ? (ShadowThicknessOverride ?? DefaultShadowThickness)
+ ? LayoutHelper.RoundLayoutThickness(ShadowThicknessOverride ?? DefaultShadowThickness, scale)
: default;
EffectiveGeometryChanged?.Invoke();
diff --git a/src/Avalonia.Controls/PresentationSource/PresentationSource.RenderRoot.cs b/src/Avalonia.Controls/PresentationSource/PresentationSource.RenderRoot.cs
index 7c129742b8..2a9e833a48 100644
--- a/src/Avalonia.Controls/PresentationSource/PresentationSource.RenderRoot.cs
+++ b/src/Avalonia.Controls/PresentationSource/PresentationSource.RenderRoot.cs
@@ -1,3 +1,4 @@
+using Avalonia.Input;
using System;
using Avalonia.Layout;
using Avalonia.Rendering;
@@ -7,7 +8,6 @@ namespace Avalonia.Controls;
internal partial class PresentationSource
{
- private readonly Func _clientSizeProvider;
public CompositingRenderer Renderer { get; }
IRenderer IPresentationSource.Renderer => Renderer;
Visual IPresentationSource.RootVisual => RootVisual;
@@ -16,8 +16,7 @@ internal partial class PresentationSource
public IHitTester? HitTesterOverride { get; set; }
public double RenderScaling { get; private set; } = 1.0;
-
- public Size ClientSize => _clientSizeProvider();
+ public Size ClientSize => PlatformImpl?.ClientSize ?? default;
public void SceneInvalidated(object? sender, SceneInvalidatedEventArgs sceneInvalidatedEventArgs)
{
diff --git a/src/Avalonia.Controls/PresentationSource/PresentationSource.cs b/src/Avalonia.Controls/PresentationSource/PresentationSource.cs
index aad1bc1003..8f298d2e30 100644
--- a/src/Avalonia.Controls/PresentationSource/PresentationSource.cs
+++ b/src/Avalonia.Controls/PresentationSource/PresentationSource.cs
@@ -23,10 +23,8 @@ internal partial class PresentationSource : IPresentationSource, IInputRoot, IDi
public PresentationSource(InputElement rootVisual, InputElement defaultFocusVisual,
ITopLevelImpl platformImpl,
- IAvaloniaDependencyResolver dependencyResolver, Func clientSizeProvider)
+ IAvaloniaDependencyResolver dependencyResolver)
{
- _clientSizeProvider = clientSizeProvider;
-
PlatformImpl = platformImpl;
diff --git a/src/Avalonia.Controls/TopLevel.cs b/src/Avalonia.Controls/TopLevel.cs
index 69d32d2b56..44b82c886f 100644
--- a/src/Avalonia.Controls/TopLevel.cs
+++ b/src/Avalonia.Controls/TopLevel.cs
@@ -212,7 +212,7 @@ namespace Avalonia.Controls
LogicalChildren.Add(hostVisual);
_source = new PresentationSource(hostVisual, this,
- impl, dependencyResolver, () => ClientSize);
+ impl, dependencyResolver);
_source.Renderer.SceneInvalidated += SceneInvalidated;
_scaling = LayoutHelper.ValidateScaling(impl.RenderScaling);
diff --git a/src/Avalonia.Controls/TopLevelHost.cs b/src/Avalonia.Controls/TopLevelHost.cs
index d2d3ddf8fa..9592dd8221 100644
--- a/src/Avalonia.Controls/TopLevelHost.cs
+++ b/src/Avalonia.Controls/TopLevelHost.cs
@@ -3,6 +3,7 @@ using System.Collections.Generic;
using Avalonia.Automation.Peers;
using Avalonia.Controls.Chrome;
using Avalonia.Input;
+using Avalonia.Layout;
using Avalonia.LogicalTree;
using Avalonia.Reactive;
@@ -14,6 +15,8 @@ namespace Avalonia.Controls;
///
internal partial class TopLevelHost : Control
{
+ private Thickness _decorationInset;
+
static TopLevelHost()
{
KeyboardNavigation.TabNavigationProperty.OverrideDefaultValue(KeyboardNavigationMode.Cycle);
@@ -25,5 +28,89 @@ internal partial class TopLevelHost : Control
VisualChildren.Add(tl);
}
+ ///
+ /// Gets or sets the decoration inset applied to the TopLevel child in forced decoration mode.
+ /// When non-zero, the TopLevel is measured and arranged within the inset area while
+ /// decoration layers use the full available size.
+ ///
+ internal Thickness DecorationInset
+ {
+ get => _decorationInset;
+ set
+ {
+ if (_decorationInset == value)
+ return;
+ _decorationInset = value;
+ InvalidateMeasure();
+ }
+ }
+
+ protected override Size MeasureOverride(Size availableSize)
+ {
+ var inset = _decorationInset;
+ var hasInset = inset != default;
+ var desiredSize = default(Size);
+
+ foreach (var child in VisualChildren)
+ {
+ if (child is Layoutable l)
+ {
+ if (hasInset && ReferenceEquals(child, _topLevel))
+ {
+ // In forced mode, measure the TopLevel with reduced size
+ var contentSize = new Size(
+ Math.Max(0, availableSize.Width - inset.Left - inset.Right),
+ Math.Max(0, availableSize.Height - inset.Top - inset.Bottom));
+ l.Measure(contentSize);
+
+ // Add inset back so TopLevelHost's desired size represents the full frame.
+ // This ensures ArrangeOverride receives the full frame size and can correctly
+ // position the TopLevel within the inset area.
+ desiredSize = new Size(
+ Math.Max(desiredSize.Width, l.DesiredSize.Width + inset.Left + inset.Right),
+ Math.Max(desiredSize.Height, l.DesiredSize.Height + inset.Top + inset.Bottom));
+ }
+ else
+ {
+ l.Measure(availableSize);
+
+ desiredSize = new Size(
+ Math.Max(desiredSize.Width, l.DesiredSize.Width),
+ Math.Max(desiredSize.Height, l.DesiredSize.Height));
+ }
+ }
+ }
+
+ return desiredSize;
+ }
+
+ protected override Size ArrangeOverride(Size finalSize)
+ {
+ var inset = _decorationInset;
+ var hasInset = inset != default;
+
+ foreach (var child in VisualChildren)
+ {
+ if (child is Layoutable l)
+ {
+ if (hasInset && ReferenceEquals(child, _topLevel))
+ {
+ // In forced mode, arrange the TopLevel within the inset area
+ var contentSize = new Size(
+ Math.Max(0, finalSize.Width - inset.Left - inset.Right),
+ Math.Max(0, finalSize.Height - inset.Top - inset.Bottom));
+
+ l.Arrange(new Rect(inset.Left, inset.Top, contentSize.Width, contentSize.Height));
+ }
+ else
+ {
+ l.Arrange(new Rect(finalSize));
+ }
+ }
+ }
+
+ return finalSize;
+ }
+
protected override bool BypassFlowDirectionPolicies => true;
}
diff --git a/src/Avalonia.Controls/Window.cs b/src/Avalonia.Controls/Window.cs
index 80349634b7..4dab3574eb 100644
--- a/src/Avalonia.Controls/Window.cs
+++ b/src/Avalonia.Controls/Window.cs
@@ -98,6 +98,7 @@ namespace Avalonia.Controls
private Thickness _offScreenMargin;
private bool _canHandleResized = false;
private Size _arrangeBounds;
+ private bool _isForcedDecorationMode;
///
/// Defines the property.
@@ -249,7 +250,11 @@ namespace Avalonia.Controls
impl.WindowStateChanged = HandleWindowStateChanged;
_maxPlatformClientSize = PlatformImpl?.MaxAutoSizeHint ?? default(Size);
impl.ExtendClientAreaToDecorationsChanged = ExtendClientAreaToDecorationsChanged;
- this.GetObservable(ClientSizeProperty).Skip(1).Subscribe(x => PlatformImpl?.Resize(x, WindowResizeReason.Application));
+ this.GetObservable(ClientSizeProperty).Skip(1).Subscribe(x =>
+ {
+ ResizePlatformImpl(x, WindowResizeReason.Application);
+ });
+ ScalingChanged += OnScalingChangedUpdateDecorations;
CreatePlatformImplBinding(TitleProperty, title => PlatformImpl!.SetTitle(title));
CreatePlatformImplBinding(IconProperty, SetEffectiveIcon);
@@ -668,7 +673,7 @@ namespace Avalonia.Controls
UpdateDrawnDecorationParts();
}
- protected virtual void ExtendClientAreaToDecorationsChanged(bool isExtended)
+ private void ExtendClientAreaToDecorationsChanged(bool isExtended)
{
IsExtendedIntoWindowDecorations = isExtended;
OffScreenMargin = PlatformImpl?.OffScreenMargin ?? default;
@@ -679,6 +684,10 @@ namespace Avalonia.Controls
private void UpdateDrawnDecorations()
{
var parts = ComputeDecorationParts();
+
+ // Detect forced mode: platform needs managed decorations but app hasn't opted in
+ _isForcedDecorationMode = parts != null && !IsExtendedIntoWindowDecorations;
+
TopLevelHost.UpdateDrawnDecorations(parts, WindowState);
if (parts != null)
@@ -687,6 +696,8 @@ namespace Avalonia.Controls
var decorations = TopLevelHost.Decorations;
if (decorations != null)
{
+ decorations.RenderScaling = RenderScaling;
+
var hint = ExtendClientAreaTitleBarHeightHint;
if (hint >= 0)
decorations.TitleBarHeightOverride = hint;
@@ -696,6 +707,13 @@ namespace Avalonia.Controls
UpdateDrawnDecorationMargins();
}
+ private void OnScalingChangedUpdateDecorations(object? sender, EventArgs e)
+ {
+ var decorations = TopLevelHost.Decorations;
+ if (decorations != null)
+ decorations.RenderScaling = RenderScaling;
+ }
+
///
/// Updates decoration parts based on current window state without
/// re-creating the decorations instance.
@@ -753,7 +771,9 @@ namespace Avalonia.Controls
var decorations = TopLevelHost.Decorations;
if (decorations == null)
{
+ // Only use platform margins if drawn decorations are not active
WindowDecorationMargin = PlatformImpl?.ExtendedMargins ?? default;
+ TopLevelHost.DecorationInset = default;
return;
}
@@ -764,11 +784,25 @@ namespace Avalonia.Controls
? decorations.FrameThickness : default;
var shadow = parts.HasFlag(Chrome.DrawnWindowDecorationParts.Shadow)
? decorations.ShadowThickness : default;
- WindowDecorationMargin = new Thickness(
+ var margin = new Thickness(
frame.Left + shadow.Left,
titleBarHeight + frame.Top + shadow.Top,
frame.Right + shadow.Right,
frame.Bottom + shadow.Bottom);
+
+ if (_isForcedDecorationMode)
+ {
+ // In forced mode, app is unaware of decorations.
+ // TopLevelHost insets the Window child; WindowDecorationMargin stays zero.
+ WindowDecorationMargin = default;
+ TopLevelHost.DecorationInset = margin;
+ }
+ else
+ {
+ // In extended mode, app handles the margin itself.
+ WindowDecorationMargin = margin;
+ TopLevelHost.DecorationInset = default;
+ }
}
private void OnTitleBarHeightHintChanged()
@@ -933,6 +967,15 @@ namespace Avalonia.Controls
// Enable drawn decorations before layout so margins are computed
UpdateDrawnDecorations();
+ // In forced mode, adjust ClientSize to reflect usable content area
+ if (_isForcedDecorationMode)
+ {
+ var inset = TopLevelHost.DecorationInset;
+ ClientSize = new Size(
+ Math.Max(0, ClientSize.Width - inset.Left - inset.Right),
+ Math.Max(0, ClientSize.Height - inset.Top - inset.Bottom));
+ }
+
_shown = true;
IsVisible = true;
@@ -978,10 +1021,18 @@ namespace Avalonia.Controls
DesktopScalingOverride = null;
- if (clientSizeChanged || ClientSize != PlatformImpl?.ClientSize)
+ // In forced mode, compare against adjusted platform size
+ var platformClientSize = PlatformImpl?.ClientSize ?? default;
+ var comparableClientSize = _isForcedDecorationMode
+ ? new Size(
+ Math.Max(0, platformClientSize.Width - TopLevelHost.DecorationInset.Left - TopLevelHost.DecorationInset.Right),
+ Math.Max(0, platformClientSize.Height - TopLevelHost.DecorationInset.Top - TopLevelHost.DecorationInset.Bottom))
+ : platformClientSize;
+
+ if (clientSizeChanged || ClientSize != comparableClientSize)
{
// Previously it was called before ExecuteInitialLayoutPass
- PlatformImpl?.Resize(ClientSize, WindowResizeReason.Layout);
+ ResizePlatformImpl(ClientSize, WindowResizeReason.Layout);
// we do not want PlatformImpl?.Resize to trigger HandleResized yet because it will set Width and Height.
// So perform some important actions from HandleResized
@@ -1037,6 +1088,22 @@ namespace Avalonia.Controls
}
}
+ private void ResizePlatformImpl(Size size, WindowResizeReason reason)
+ {
+ // In forced mode, add decoration inset so platform gets full frame size
+ if (_isForcedDecorationMode)
+ {
+ var inset = TopLevelHost.DecorationInset;
+ size = new Size(
+ size.Width + inset.Left + inset.Right,
+ size.Height + inset.Top + inset.Bottom);
+ if (PlatformImpl?.ClientSize != size)
+ PlatformImpl?.Resize(size, reason);
+ }
+ else
+ PlatformImpl?.Resize(size, reason);
+ }
+
///
/// Shows the window as a dialog.
///
@@ -1272,6 +1339,14 @@ namespace Avalonia.Controls
{
var sizeToContent = SizeToContent;
var clientSize = ClientSize;
+ if (_isForcedDecorationMode)
+ {
+ clientSize = PlatformImpl?.ClientSize ?? clientSize;
+ var inset = TopLevelHost.DecorationInset;
+ clientSize = new Size(
+ Math.Max(0, clientSize.Width - inset.Left - inset.Right),
+ Math.Max(0, clientSize.Height - inset.Top - inset.Bottom));
+ }
var maxAutoSize = PlatformImpl?.MaxAutoSizeHint ?? Size.Infinity;
var useAutoWidth = sizeToContent.HasAllFlags(SizeToContent.Width);
var useAutoHeight = sizeToContent.HasAllFlags(SizeToContent.Height);
@@ -1332,7 +1407,9 @@ namespace Avalonia.Controls
{
_arrangeBounds = size;
if (_canHandleResized)
- PlatformImpl?.Resize(size, WindowResizeReason.Layout);
+ {
+ ResizePlatformImpl(size, WindowResizeReason.Layout);
+ }
return ClientSize;
}
@@ -1350,6 +1427,16 @@ namespace Avalonia.Controls
///
internal override void HandleResized(Size clientSize, WindowResizeReason reason)
{
+ // In forced decoration mode, the platform's clientSize includes decoration area.
+ // Subtract the decoration inset so Window.ClientSize reflects the usable content area.
+ if (_isForcedDecorationMode)
+ {
+ var inset = TopLevelHost.DecorationInset;
+ clientSize = new Size(
+ Math.Max(0, clientSize.Width - inset.Left - inset.Right),
+ Math.Max(0, clientSize.Height - inset.Top - inset.Bottom));
+ }
+
if (_canHandleResized && (ClientSize != clientSize || double.IsNaN(Width) || double.IsNaN(Height)))
{
var sizeToContent = SizeToContent;
diff --git a/src/Avalonia.Controls/WindowBase.cs b/src/Avalonia.Controls/WindowBase.cs
index 07966600e6..894b7ca6e6 100644
--- a/src/Avalonia.Controls/WindowBase.cs
+++ b/src/Avalonia.Controls/WindowBase.cs
@@ -304,7 +304,7 @@ namespace Avalonia.Controls
{
var constraint = ArrangeSetBounds(finalRect.Size);
var arrangeSize = ArrangeOverride(constraint);
- Bounds = new Rect(arrangeSize);
+ Bounds = new Rect(finalRect.Position, arrangeSize);
}
///
diff --git a/src/Avalonia.X11/X11Platform.cs b/src/Avalonia.X11/X11Platform.cs
index 566b0d907a..a3f98265f4 100644
--- a/src/Avalonia.X11/X11Platform.cs
+++ b/src/Avalonia.X11/X11Platform.cs
@@ -482,13 +482,32 @@ namespace Avalonia
, Message = "Experimental, used mostly for testing"
#endif
)]
- public bool? EnableDrawnDecorations
- {
- get => EnableDrawnDecorationsInternal;
- set => EnableDrawnDecorationsInternal = value;
- }
+ public bool? EnableDrawnDecorations { get; set; }
+
+ internal bool EnableDrawnDecorationsInternal =>
+#pragma warning disable AVALONIA_X11_CSD
+ EnableDrawnDecorations == true || ForceDrawnDecorationsInternal;
+#pragma warning restore AVALONIA_X11_CSD
+
+
+ ///
+ /// Forces client-side drawn window decorations on X11 for all windows,
+ /// even when the app has not opted in via ExtendClientAreaToDecorationsHint.
+ /// In this mode, Window.ClientSize reflects the usable content area
+ /// (platform client size minus decoration margins) and the app is unaware
+ /// of the decorations.
+ /// Implies EnableDrawnDecorations = true.
+ ///
+ [Experimental("AVALONIA_X11_FORCE_CSD"
+ #if NET10_0_OR_GREATER
+ , Message = "Experimental, used mostly for testing"
+ #endif
+ )]
+ public bool ForceDrawnDecorations { get; set; }
- internal bool? EnableDrawnDecorationsInternal { get; set; }
+#pragma warning disable AVALONIA_X11_FORCE_CSD
+ internal bool ForceDrawnDecorationsInternal => ForceDrawnDecorations;
+#pragma warning restore AVALONIA_X11_FORCE_CSD
///
/// If Avalonia is in control of a run loop, we propagate exceptions by stopping the run loop frame
diff --git a/src/Avalonia.X11/X11Window.cs b/src/Avalonia.X11/X11Window.cs
index 0cfa226163..23c5cc18db 100644
--- a/src/Avalonia.X11/X11Window.cs
+++ b/src/Avalonia.X11/X11Window.cs
@@ -926,7 +926,7 @@ namespace Avalonia.X11
mouse.Position = mouse.Position / RenderScaling;
// Chrome hit-test for drawn decorations
- if (_extendClientAreaToDecorations
+ if (UseManagedDecorations
&& mouse.Type == RawPointerEventType.LeftButtonDown
&& _inputRoot is { } inputRoot)
{
@@ -1187,8 +1187,8 @@ namespace Avalonia.X11
private void UpdateEffectiveSystemDecorations()
{
- // When extending client area, always hide WM decorations (we draw our own)
- var effective = _extendClientAreaToDecorations
+ // When extending client area or forcing drawn decorations, always hide WM decorations (we draw our own)
+ var effective = UseManagedDecorations
? WindowDecorations.None
: (_requestedWindowDecorations == WindowDecorations.Full
? WindowDecorations.Full
@@ -1509,17 +1509,18 @@ namespace Avalonia.X11
}
}
- private bool _extendClientAreaToDecorations;
+ private bool _extendingClientAreaToDecorations;
+ private bool UseManagedDecorations => _extendingClientAreaToDecorations || _platform.Options.ForceDrawnDecorationsInternal;
public void SetExtendClientAreaToDecorationsHint(bool extendIntoClientAreaHint)
{
- if (_platform.Options.EnableDrawnDecorationsInternal != true)
+ if (!_platform.Options.EnableDrawnDecorationsInternal)
return;
- if (_extendClientAreaToDecorations == extendIntoClientAreaHint)
+ if (_extendingClientAreaToDecorations == extendIntoClientAreaHint)
return;
- _extendClientAreaToDecorations = extendIntoClientAreaHint;
+ _extendingClientAreaToDecorations = extendIntoClientAreaHint;
UpdateEffectiveSystemDecorations();
IsClientAreaExtendedToDecorations = extendIntoClientAreaHint;
@@ -1606,10 +1607,10 @@ namespace Avalonia.X11
public AcrylicPlatformCompensationLevels AcrylicCompensationLevels { get; } = new AcrylicPlatformCompensationLevels(1, 0.8, 0.8);
- public bool NeedsManagedDecorations => _extendClientAreaToDecorations;
+ public bool NeedsManagedDecorations => UseManagedDecorations;
public PlatformRequestedDrawnDecoration RequestedDrawnDecorations =>
- _extendClientAreaToDecorations
+ UseManagedDecorations
? PlatformRequestedDrawnDecoration.Border
| PlatformRequestedDrawnDecoration.ResizeGrips
| PlatformRequestedDrawnDecoration.TitleBar
diff --git a/src/Windows/Avalonia.Win32/WindowImpl.cs b/src/Windows/Avalonia.Win32/WindowImpl.cs
index 0882516f57..9233cc2809 100644
--- a/src/Windows/Avalonia.Win32/WindowImpl.cs
+++ b/src/Windows/Avalonia.Win32/WindowImpl.cs
@@ -1188,6 +1188,7 @@ namespace Avalonia.Win32
{
if (!_shown)
{
+ ExtendClientAreaToDecorationsChanged?.Invoke(_isClientAreaExtended);
return;
}
diff --git a/tests/Avalonia.Controls.UnitTests/WindowTests.cs b/tests/Avalonia.Controls.UnitTests/WindowTests.cs
index 5347acbc33..63cc2db193 100644
--- a/tests/Avalonia.Controls.UnitTests/WindowTests.cs
+++ b/tests/Avalonia.Controls.UnitTests/WindowTests.cs
@@ -2,6 +2,7 @@ using System;
using System.Collections.Generic;
using System.Reactive.Linq;
using System.Threading.Tasks;
+using Avalonia.Controls.Platform;
using Avalonia.Media;
using Avalonia.Platform;
using Avalonia.Threading;
@@ -1164,6 +1165,236 @@ namespace Avalonia.Controls.UnitTests
}
}
+ public class ForcedDecorationSizingTests : ScopedTestBase
+ {
+ ///
+ /// Creates a mock IWindowImpl that simulates forced CSD mode:
+ /// NeedsManagedDecorations = true, RequestedDrawnDecorations includes TitleBar + Border,
+ /// but IsClientAreaExtendedToDecorations = false.
+ ///
+ private static Mock CreateForcedCsdWindowMock(
+ double initialWidth = 800, double initialHeight = 600)
+ {
+ var windowImpl = MockWindowingPlatform.CreateWindowMock(initialWidth, initialHeight);
+
+ windowImpl.Setup(x => x.NeedsManagedDecorations).Returns(true);
+ windowImpl.Setup(x => x.RequestedDrawnDecorations).Returns(
+ PlatformRequestedDrawnDecoration.TitleBar | PlatformRequestedDrawnDecoration.Border);
+
+ return windowImpl;
+ }
+
+ [Fact]
+ public void ClientSize_Should_Exclude_Decoration_Inset()
+ {
+ using (UnitTestApplication.Start(TestServices.StyledWindow))
+ {
+ var windowImpl = CreateForcedCsdWindowMock();
+ var target = new Window(windowImpl.Object)
+ {
+ SizeToContent = SizeToContent.Manual,
+ };
+
+ // Verify mock setup
+ Assert.True(windowImpl.Object.NeedsManagedDecorations);
+
+ target.Show();
+
+ var host = target.TopLevelHost;
+ var decorations = host.Decorations;
+
+ // Debug: verify decorations were created
+ Assert.NotNull(decorations);
+ Assert.True(decorations!.TitleBarHeight > 0,
+ $"TitleBarHeight was {decorations.TitleBarHeight}");
+
+ var inset = host.DecorationInset;
+ Assert.NotEqual(default, inset);
+
+ var expectedClientSize = new Size(
+ 800 - inset.Left - inset.Right,
+ 600 - inset.Top - inset.Bottom);
+ Assert.Equal(expectedClientSize, target.ClientSize);
+ }
+ }
+
+ [Fact]
+ public void WindowDecorationMargin_Should_Be_Zero_In_Forced_Mode()
+ {
+ using (UnitTestApplication.Start(TestServices.StyledWindow))
+ {
+ var windowImpl = CreateForcedCsdWindowMock();
+ var target = new Window(windowImpl.Object)
+ {
+ SizeToContent = SizeToContent.Manual,
+ };
+
+ target.Show();
+
+ Assert.Equal(default(Thickness), target.WindowDecorationMargin);
+ }
+ }
+
+ [Fact]
+ public void HandleResized_Should_Subtract_Inset_From_Platform_Size()
+ {
+ using (UnitTestApplication.Start(TestServices.StyledWindow))
+ {
+ var windowImpl = CreateForcedCsdWindowMock();
+ var target = new Window(windowImpl.Object)
+ {
+ SizeToContent = SizeToContent.Manual,
+ };
+
+ target.Show();
+
+ var inset = target.TopLevelHost.DecorationInset;
+
+ // Simulate a platform resize (e.g. user resize)
+ target.PlatformImpl!.Resized!.Invoke(new Size(1000, 700), WindowResizeReason.User);
+
+ var expectedClientSize = new Size(
+ 1000 - inset.Left - inset.Right,
+ 700 - inset.Top - inset.Bottom);
+ Assert.Equal(expectedClientSize, target.ClientSize);
+ }
+ }
+
+ [Fact]
+ public void Setting_Width_Should_Resize_WindowImpl_With_Inset_Added()
+ {
+ using (UnitTestApplication.Start(TestServices.StyledWindow))
+ {
+ var windowImpl = CreateForcedCsdWindowMock();
+ var target = new Window(windowImpl.Object)
+ {
+ Width = 400,
+ Height = 300,
+ SizeToContent = SizeToContent.Manual,
+ };
+
+ target.Show();
+
+ var inset = target.TopLevelHost.DecorationInset;
+
+ target.Width = 500;
+ target.LayoutManager.ExecuteLayoutPass();
+
+ // Platform should receive full frame size (content + inset)
+ var expectedPlatformSize = new Size(
+ 500 + inset.Left + inset.Right,
+ 300 + inset.Top + inset.Bottom);
+ windowImpl.Verify(x => x.Resize(expectedPlatformSize, WindowResizeReason.Layout));
+ }
+ }
+
+ [Fact]
+ public void Child_Should_Be_Measured_With_Content_Size()
+ {
+ using (UnitTestApplication.Start(TestServices.StyledWindow))
+ {
+ var windowImpl = CreateForcedCsdWindowMock();
+ var child = new ChildControl();
+ var target = new Window(windowImpl.Object)
+ {
+ Width = 400,
+ Height = 300,
+ SizeToContent = SizeToContent.Manual,
+ Content = child,
+ };
+
+ target.Show();
+
+ Assert.Equal(1, child.MeasureSizes.Count);
+ Assert.Equal(new Size(400, 300), child.MeasureSizes[0]);
+ }
+ }
+
+ [Fact]
+ public void Width_Height_Should_Not_Be_NaN_After_Show()
+ {
+ using (UnitTestApplication.Start(TestServices.StyledWindow))
+ {
+ var windowImpl = CreateForcedCsdWindowMock();
+ var target = new Window(windowImpl.Object)
+ {
+ SizeToContent = SizeToContent.Manual,
+ };
+
+ target.Show();
+
+ Assert.False(double.IsNaN(target.Width));
+ Assert.False(double.IsNaN(target.Height));
+
+ var inset = target.TopLevelHost.DecorationInset;
+ Assert.Equal(800 - inset.Left - inset.Right, target.Width);
+ Assert.Equal(600 - inset.Top - inset.Bottom, target.Height);
+ }
+ }
+
+ [Fact]
+ public void SizeToContent_Should_Work_In_Forced_Mode()
+ {
+ using (UnitTestApplication.Start(TestServices.StyledWindow))
+ {
+ var windowImpl = CreateForcedCsdWindowMock();
+ var child = new Canvas
+ {
+ Width = 400,
+ Height = 300,
+ };
+
+ var target = new Window(windowImpl.Object)
+ {
+ SizeToContent = SizeToContent.WidthAndHeight,
+ Content = child,
+ };
+
+ target.Show();
+
+ Assert.Equal(400, target.Width);
+ Assert.Equal(300, target.Height);
+ Assert.Equal(SizeToContent.WidthAndHeight, target.SizeToContent);
+ }
+ }
+
+ [Fact]
+ public void User_Resize_Should_Reset_SizeToContent()
+ {
+ using (UnitTestApplication.Start(TestServices.StyledWindow))
+ {
+ var windowImpl = CreateForcedCsdWindowMock();
+ var child = new Canvas
+ {
+ Width = 400,
+ Height = 300,
+ };
+
+ var target = new Window(windowImpl.Object)
+ {
+ SizeToContent = SizeToContent.WidthAndHeight,
+ Content = child,
+ };
+
+ target.Show();
+ Assert.Equal(400, target.Width);
+ Assert.Equal(300, target.Height);
+
+ var inset = target.TopLevelHost.DecorationInset;
+ // Platform fires resize with full frame size
+ var newPlatformWidth = 500 + inset.Left + inset.Right;
+ var newPlatformHeight = 300 + inset.Top + inset.Bottom;
+ windowImpl.Object.Resized?.Invoke(
+ new Size(newPlatformWidth, newPlatformHeight),
+ WindowResizeReason.User);
+
+ Assert.Equal(500, target.Width);
+ Assert.Equal(300, target.Height);
+ Assert.Equal(SizeToContent.Height, target.SizeToContent);
+ }
+ }
+ }
+
private static Mock CreateImpl()
{
var screen1 = new MockScreen(1.75, new PixelRect(new PixelSize(1920, 1080)), new PixelRect(new PixelSize(1920, 966)), true);