Skip to content
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions api/Avalonia.nupkg.xml
Original file line number Diff line number Diff line change
Expand Up @@ -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>
Expand Down Expand Up @@ -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>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
using System;
using Avalonia.Input;
using Avalonia.Rendering;
using Avalonia.Rendering.Composition;
Expand All @@ -7,7 +6,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;
Expand All @@ -16,7 +14,7 @@ internal partial class PresentationSource
public IHitTester? HitTesterOverride { get; set; }

public double RenderScaling => PlatformImpl?.RenderScaling ?? 1;
public Size ClientSize => _clientSizeProvider();
public Size ClientSize => PlatformImpl?.ClientSize ?? default;

public void SceneInvalidated(object? sender, SceneInvalidatedEventArgs sceneInvalidatedEventArgs)
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;


Expand Down
2 changes: 1 addition & 1 deletion src/Avalonia.Controls/TopLevel.cs
Original file line number Diff line number Diff line change
Expand Up @@ -212,7 +212,7 @@ internal TopLevel(ITopLevelImpl impl, IAvaloniaDependencyResolver? dependencyRes
LogicalChildren.Add(hostVisual);

_source = new PresentationSource(hostVisual, this,
impl, dependencyResolver, () => ClientSize);
impl, dependencyResolver);
_source.Renderer.SceneInvalidated += SceneInvalidated;

_scaling = ValidateScaling(impl.RenderScaling);
Expand Down
87 changes: 87 additions & 0 deletions src/Avalonia.Controls/TopLevelHost.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
using Avalonia.Automation.Peers;
using Avalonia.Controls.Chrome;
using Avalonia.Input;
using Avalonia.Layout;
using Avalonia.LogicalTree;
using Avalonia.Reactive;

Expand All @@ -14,6 +15,8 @@ namespace Avalonia.Controls;
/// </summary>
internal partial class TopLevelHost : Control
{
private Thickness _decorationInset;

static TopLevelHost()
{
KeyboardNavigation.TabNavigationProperty.OverrideDefaultValue<TopLevelHost>(KeyboardNavigationMode.Cycle);
Expand All @@ -25,5 +28,89 @@ public TopLevelHost(TopLevel tl)
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;
Comment thread
MrJul marked this conversation as resolved.
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;
}
89 changes: 83 additions & 6 deletions src/Avalonia.Controls/Window.cs
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,7 @@ public class Window : WindowBase, IFocusScope
private Thickness _offScreenMargin;
private bool _canHandleResized = false;
private Size _arrangeBounds;
private bool _isForcedDecorationMode;

/// <summary>
/// Defines the <see cref="SizeToContent"/> property.
Expand Down Expand Up @@ -249,7 +250,10 @@ public Window(IWindowImpl impl)
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);
});

CreatePlatformImplBinding(TitleProperty, title => PlatformImpl!.SetTitle(title));
CreatePlatformImplBinding(IconProperty, SetEffectiveIcon);
Expand Down Expand Up @@ -668,7 +672,7 @@ private void HandleWindowStateChanged(WindowState state)
UpdateDrawnDecorationParts();
}

protected virtual void ExtendClientAreaToDecorationsChanged(bool isExtended)
private void ExtendClientAreaToDecorationsChanged(bool isExtended)
{
IsExtendedIntoWindowDecorations = isExtended;
OffScreenMargin = PlatformImpl?.OffScreenMargin ?? default;
Expand All @@ -679,6 +683,10 @@ protected virtual void ExtendClientAreaToDecorationsChanged(bool isExtended)
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)
Expand Down Expand Up @@ -753,7 +761,9 @@ private void UpdateDrawnDecorationMargins()
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;
}

Expand All @@ -764,11 +774,25 @@ private void UpdateDrawnDecorationMargins()
? 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;
}
Comment on lines +793 to +805

Copilot AI Mar 25, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In forced-decoration mode, TopLevelHost.DecorationInset can change at runtime (theme changes, TitleBarHeightOverride, enabled parts changes). The code updates DecorationInset but doesn’t recompute Window.ClientSize/Width/Height to match PlatformImpl.ClientSize - inset, so subsequent arrange passes can use a stale ClientSize (e.g. ArrangeSetBounds returns ClientSize) and the window’s layout/renderer size can get out of sync with the actual usable content area. Consider updating the forced-mode code path to recalculate ClientSize (and raise the normal resize pipeline) whenever the inset changes while shown.

Copilot uses AI. Check for mistakes.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Force-updating ClientSize here causes inconsistency when enabling/disabling extend-client-area-into-decorations.

@kekekeks kekekeks Mar 25, 2026

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In general shouldn't we update the window size instead of client size here?

@kekekeks kekekeks Mar 25, 2026

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This might cause issues with Maximize/Fullscreen. Since both are working now, I'm opting to leave this as-is. ClientSize eventually fixes itself since this triggers a layout pass and window resize later.

}

private void OnTitleBarHeightHintChanged()
Expand Down Expand Up @@ -933,6 +957,15 @@ private void EnsureParentStateBeforeShow(Window owner)
// 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;

Expand Down Expand Up @@ -978,10 +1011,18 @@ private void EnsureParentStateBeforeShow(Window owner)

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
Expand Down Expand Up @@ -1037,6 +1078,22 @@ private void EnsureParentStateBeforeShow(Window owner)
}
}

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>
Expand Down Expand Up @@ -1272,6 +1329,14 @@ protected override Size MeasureOverride(Size availableSize)
{
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);
Expand Down Expand Up @@ -1332,7 +1397,9 @@ private protected sealed override Size ArrangeSetBounds(Size size)
{
_arrangeBounds = size;
if (_canHandleResized)
PlatformImpl?.Resize(size, WindowResizeReason.Layout);
{
ResizePlatformImpl(size, WindowResizeReason.Layout);
}
return ClientSize;
}

Expand All @@ -1350,6 +1417,16 @@ private protected sealed override void HandleClosed()
/// <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;
Expand Down
2 changes: 1 addition & 1 deletion src/Avalonia.Controls/WindowBase.cs
Original file line number Diff line number Diff line change
Expand Up @@ -304,7 +304,7 @@ protected override void ArrangeCore(Rect finalRect)
{
var constraint = ArrangeSetBounds(finalRect.Size);
var arrangeSize = ArrangeOverride(constraint);
Bounds = new Rect(arrangeSize);
Bounds = new Rect(finalRect.Position, arrangeSize);
}

/// <summary>
Expand Down
Loading
Loading