Browse Source

Introduced "forced" CSD mode without app opting in (#20976)

* Introduced "forced" CSD mode without app opting in

* C is for Consistency

* api diff

* [X11] Better handling of forced-vs-app-triggeed CSD

* Round WindowDrawnDecorations sizes to be pixel-aligned
pull/20996/head
Nikita Tsukanov 1 day ago
committed by GitHub
parent
commit
4d13770b34
No known key found for this signature in database GPG Key ID: B5690EEEBB952194
  1. 12
      api/Avalonia.nupkg.xml
  2. 27
      src/Avalonia.Controls/Chrome/WindowDrawnDecorations.cs
  3. 5
      src/Avalonia.Controls/PresentationSource/PresentationSource.RenderRoot.cs
  4. 4
      src/Avalonia.Controls/PresentationSource/PresentationSource.cs
  5. 2
      src/Avalonia.Controls/TopLevel.cs
  6. 87
      src/Avalonia.Controls/TopLevelHost.cs
  7. 99
      src/Avalonia.Controls/Window.cs
  8. 2
      src/Avalonia.Controls/WindowBase.cs
  9. 31
      src/Avalonia.X11/X11Platform.cs
  10. 19
      src/Avalonia.X11/X11Window.cs
  11. 1
      src/Windows/Avalonia.Win32/WindowImpl.cs
  12. 231
      tests/Avalonia.Controls.UnitTests/WindowTests.cs

12
api/Avalonia.nupkg.xml

@ -2395,6 +2395,12 @@
<Left>baseline/Avalonia/lib/net10.0/Avalonia.Controls.dll</Left>
<Right>current/Avalonia/lib/net10.0/Avalonia.Controls.dll</Right>
</Suppression>
<Suppression>
<DiagnosticId>CP0002</DiagnosticId>
<Target>M:Avalonia.Controls.Window.ExtendClientAreaToDecorationsChanged(System.Boolean)</Target>
<Left>baseline/Avalonia/lib/net10.0/Avalonia.Controls.dll</Left>
<Right>current/Avalonia/lib/net10.0/Avalonia.Controls.dll</Right>
</Suppression>
<Suppression>
<DiagnosticId>CP0002</DiagnosticId>
<Target>M:Avalonia.Controls.Window.get_ExtendClientAreaChromeHints</Target>
@ -4063,6 +4069,12 @@
<Left>baseline/Avalonia/lib/net8.0/Avalonia.Controls.dll</Left>
<Right>current/Avalonia/lib/net8.0/Avalonia.Controls.dll</Right>
</Suppression>
<Suppression>
<DiagnosticId>CP0002</DiagnosticId>
<Target>M:Avalonia.Controls.Window.ExtendClientAreaToDecorationsChanged(System.Boolean)</Target>
<Left>baseline/Avalonia/lib/net8.0/Avalonia.Controls.dll</Left>
<Right>current/Avalonia/lib/net8.0/Avalonia.Controls.dll</Right>
</Suppression>
<Suppression>
<DiagnosticId>CP0002</DiagnosticId>
<Target>M:Avalonia.Controls.Window.get_ExtendClientAreaChromeHints</Target>

27
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;
/// <summary>
/// Raised when any property affecting the effective geometry changes
@ -145,6 +147,22 @@ public class WindowDrawnDecorations : StyledElement
/// </summary>
internal event Action? EffectiveGeometryChanged;
/// <summary>
/// Gets or sets the current render scaling factor used for pixel-aligning
/// decoration geometry (title bar height, frame/shadow thickness).
/// </summary>
internal double RenderScaling
{
get => _renderScaling;
set
{
if (_renderScaling == value)
return;
_renderScaling = value;
UpdateEffectiveGeometry();
}
}
/// <summary>
/// Gets or sets the decorations template.
/// </summary>
@ -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();

5
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<Size> _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)
{

4
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<Size> clientSizeProvider)
IAvaloniaDependencyResolver dependencyResolver)
{
_clientSizeProvider = clientSizeProvider;
PlatformImpl = platformImpl;

2
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);

87
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;
/// </summary>
internal partial class TopLevelHost : Control
{
private Thickness _decorationInset;
static TopLevelHost()
{
KeyboardNavigation.TabNavigationProperty.OverrideDefaultValue<TopLevelHost>(KeyboardNavigationMode.Cycle);
@ -25,5 +28,89 @@ internal partial class TopLevelHost : Control
VisualChildren.Add(tl);
}
/// <summary>
/// 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.
/// </summary>
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;
}

99
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;
/// <summary>
/// Defines the <see cref="SizeToContent"/> 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;
}
/// <summary>
/// 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);
}
/// <summary>
/// Shows the window as a dialog.
/// </summary>
@ -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
/// <inheritdoc/>
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;

2
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);
}
/// <summary>

31
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
/// <summary>
/// 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.
/// </summary>
[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
/// <summary>
/// If Avalonia is in control of a run loop, we propagate exceptions by stopping the run loop frame

19
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

1
src/Windows/Avalonia.Win32/WindowImpl.cs

@ -1188,6 +1188,7 @@ namespace Avalonia.Win32
{
if (!_shown)
{
ExtendClientAreaToDecorationsChanged?.Invoke(_isClientAreaExtended);
return;
}

231
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
{
/// <summary>
/// Creates a mock IWindowImpl that simulates forced CSD mode:
/// NeedsManagedDecorations = true, RequestedDrawnDecorations includes TitleBar + Border,
/// but IsClientAreaExtendedToDecorations = false.
/// </summary>
private static Mock<IWindowImpl> 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<IWindowImpl> CreateImpl()
{
var screen1 = new MockScreen(1.75, new PixelRect(new PixelSize(1920, 1080)), new PixelRect(new PixelSize(1920, 966)), true);

Loading…
Cancel
Save