diff --git a/src/Avalonia.Controls/Chrome/WindowDrawnDecorations.cs b/src/Avalonia.Controls/Chrome/WindowDrawnDecorations.cs index ae279d6ab3..057a2371eb 100644 --- a/src/Avalonia.Controls/Chrome/WindowDrawnDecorations.cs +++ b/src/Avalonia.Controls/Chrome/WindowDrawnDecorations.cs @@ -506,26 +506,24 @@ public class WindowDrawnDecorations : StyledElement private void OnMinimizeButtonClick(object? sender, Interactivity.RoutedEventArgs e) { - if (_hostWindow != null) - _hostWindow.WindowState = WindowState.Minimized; + _hostWindow?.TrySetWindowState(WindowState.Minimized); e.Handled = true; } private void OnMaximizeButtonClick(object? sender, Interactivity.RoutedEventArgs e) { - if (_hostWindow != null) - _hostWindow.WindowState = _hostWindow.WindowState == WindowState.Maximized - ? WindowState.Normal - : WindowState.Maximized; + _hostWindow?.TrySetWindowState(_hostWindow.EffectivePlatformWindowState == WindowState.Maximized + ? WindowState.Normal + : WindowState.Maximized); e.Handled = true; } private void OnFullScreenButtonClick(object? sender, Interactivity.RoutedEventArgs e) { if (_hostWindow != null) - _hostWindow.WindowState = _hostWindow.WindowState == WindowState.FullScreen + _hostWindow?.TrySetWindowState(_hostWindow.EffectivePlatformWindowState == WindowState.FullScreen ? WindowState.Normal - : WindowState.FullScreen; + : WindowState.FullScreen); e.Handled = true; } @@ -533,7 +531,7 @@ public class WindowDrawnDecorations : StyledElement { if (_maximizeButton == null) return; - _maximizeButton.IsEnabled = _hostWindow?.WindowState switch + _maximizeButton.IsEnabled = _hostWindow?.EffectivePlatformWindowState switch { WindowState.Maximized or WindowState.FullScreen => _hostWindow.CanResize, WindowState.Normal => _hostWindow.CanMaximize, @@ -552,7 +550,7 @@ public class WindowDrawnDecorations : StyledElement { if (_fullScreenButton == null) return; - _fullScreenButton.IsEnabled = _hostWindow?.WindowState == WindowState.FullScreen + _fullScreenButton.IsEnabled = _hostWindow?.EffectivePlatformWindowState == WindowState.FullScreen ? _hostWindow.CanResize : _hostWindow?.CanMaximize ?? true; } diff --git a/src/Avalonia.Controls/TopLevelHost.cs b/src/Avalonia.Controls/TopLevelHost.cs index ad3c812940..9592dd8221 100644 --- a/src/Avalonia.Controls/TopLevelHost.cs +++ b/src/Avalonia.Controls/TopLevelHost.cs @@ -99,16 +99,7 @@ internal partial class TopLevelHost : Control var contentSize = new Size( Math.Max(0, finalSize.Width - inset.Left - inset.Right), Math.Max(0, finalSize.Height - inset.Top - inset.Bottom)); - - // Resize the platform window only when the content actually changed size. - // During window state transitions (e.g. maximize→restore), the inset changes - // but the platform hasn't sent the new size yet — the layout runs with stale - // Width/Height. Comparing against ClientSize detects this: if only the inset - // changed, content size matches ClientSize and we skip the bogus resize. - if (contentSize != _topLevel.ClientSize - && _topLevel is Window { ResizeWindowInTopLevelHost: true } window) - window.PlatformImpl?.Resize(finalSize, WindowResizeReason.Layout); - + l.Arrange(new Rect(inset.Left, inset.Top, contentSize.Width, contentSize.Height)); } else diff --git a/src/Avalonia.Controls/Window.cs b/src/Avalonia.Controls/Window.cs index 903c4568b7..1a83be2017 100644 --- a/src/Avalonia.Controls/Window.cs +++ b/src/Avalonia.Controls/Window.cs @@ -157,11 +157,24 @@ namespace Avalonia.Controls AvaloniaProperty.Register(nameof(ClosingBehavior)); /// - /// Represents the current window state (normal, minimized, maximized) + /// Represents the current window state (normal, minimized, maximized) or window state set by data binding. + /// Can temporally have a value that had no effect on the window state whatsoever if the value + /// was set by data binding and the platform refused to apply it or delayed applying it. + /// You have no means to detect if the value of this property currently has a temporary value, + /// this is done by design to keep this property bindable. /// public static readonly StyledProperty WindowStateProperty = AvaloniaProperty.Register(nameof(WindowState)); + + private WindowState _effectivePlatformWindowStatePropertyCache; + /// + /// Represents the currently effective window state (normal, minimized, maximized) + /// + public static readonly DirectProperty EffectivePlatformWindowStateProperty = AvaloniaProperty.RegisterDirect( + "EffectivePlatformWindowState", o => o.EffectivePlatformWindowState); + + /// /// Defines the property. /// @@ -258,7 +271,7 @@ namespace Avalonia.Controls CreatePlatformImplBinding(CanMaximizeProperty, canMaximize => PlatformImpl!.SetCanMaximize(canMaximize)); CreatePlatformImplBinding(ShowInTaskbarProperty, show => PlatformImpl!.ShowTaskbarIcon(show)); - CreatePlatformImplBinding(WindowStateProperty, state => PlatformImpl!.WindowState = state); + CreatePlatformImplBinding(WindowStateProperty, TrySetWindowState); CreatePlatformImplBinding(ExtendClientAreaToDecorationsHintProperty, hint => PlatformImpl!.SetExtendClientAreaToDecorationsHint(hint)); CreatePlatformImplBinding(ExtendClientAreaTitleBarHeightHintProperty, height => PlatformImpl!.SetExtendClientAreaTitleBarHeightHint(height)); @@ -406,13 +419,22 @@ namespace Avalonia.Controls } /// - /// Gets or sets the minimized/maximized state of the window. + /// Represents the current window state (normal, minimized, maximized) or window state set by data binding. + /// Can temporally have a value that had no effect on the window state whatsoever if the value + /// was set by data binding and the platform refused to apply it or delayed applying it. + /// To get the effective window state use /// public WindowState WindowState { get => GetValue(WindowStateProperty); + [Obsolete("Use TrySetWindowState")] set => SetValue(WindowStateProperty, value); } + + /// + /// Represents the currently effective window state (normal, minimized, maximized) + /// + public WindowState EffectivePlatformWindowState => PlatformImpl?.WindowState ?? WindowState; /// /// Enables or disables resizing of the window. @@ -616,10 +638,15 @@ namespace Avalonia.Controls return false; } - + private void HandleWindowStateChanged(WindowState state) { +#pragma warning disable CS0618 // Type or member is obsolete + SetAndRaise(EffectivePlatformWindowStateProperty, ref _effectivePlatformWindowStatePropertyCache, + EffectivePlatformWindowState); + WindowState = state; +#pragma warning restore CS0618 // Type or member is obsolete if (state == WindowState.Minimized) { @@ -633,6 +660,24 @@ namespace Avalonia.Controls // Update decoration parts and fullscreen popover state for the new window state UpdateDrawnDecorationParts(); } + + private void HandleBindableWindowStateChanged(WindowState state) + { + if (PlatformImpl?.WindowState == state) + return; + PlatformImpl?.WindowState = state; + if(PlatformImpl == null || PlatformImpl.WindowState == state) + return; + // Request failed, reset WindowState to the effective value +#pragma warning disable CS0618 // Type or member is obsolete + WindowState = PlatformImpl.WindowState; +#pragma warning restore CS0618 // Type or member is obsolete + } + + public void TrySetWindowState(WindowState state) + { + PlatformImpl?.WindowState = state; + } protected virtual void ExtendClientAreaToDecorationsChanged(bool isExtended) { @@ -649,7 +694,7 @@ namespace Avalonia.Controls // Detect forced mode: platform needs managed decorations but app hasn't opted in _isForcedDecorationMode = parts != null && !IsExtendedIntoWindowDecorations; - TopLevelHost.UpdateDrawnDecorations(parts, WindowState); + TopLevelHost.UpdateDrawnDecorations(parts, EffectivePlatformWindowState); if (parts != null) { @@ -675,7 +720,7 @@ namespace Avalonia.Controls if (TopLevelHost.Decorations == null) return; - TopLevelHost.UpdateDrawnDecorations(ComputeDecorationParts(), WindowState); + TopLevelHost.UpdateDrawnDecorations(ComputeDecorationParts(), EffectivePlatformWindowState); } private Chrome.DrawnWindowDecorationParts? ComputeDecorationParts() @@ -699,7 +744,7 @@ namespace Avalonia.Controls // In fullscreen: no shadow, border, resize grips, or titlebar (popover takes over) - if (WindowState == WindowState.FullScreen) + if (EffectivePlatformWindowState == WindowState.FullScreen) { parts &= ~(Chrome.DrawnWindowDecorationParts.Shadow | Chrome.DrawnWindowDecorationParts.Border @@ -707,7 +752,7 @@ namespace Avalonia.Controls | Chrome.DrawnWindowDecorationParts.TitleBar); } // In maximized: no shadow, border, or resize grips (titlebar stays) - else if (WindowState == WindowState.Maximized) + else if (EffectivePlatformWindowState == WindowState.Maximized) { parts &= ~(Chrome.DrawnWindowDecorationParts.Shadow | Chrome.DrawnWindowDecorationParts.Border @@ -1041,8 +1086,10 @@ namespace Avalonia.Controls size = new Size( size.Width + inset.Left + inset.Right, size.Height + inset.Top + inset.Bottom); + if (PlatformImpl?.ClientSize != size) + PlatformImpl?.Resize(size, reason); } - if (PlatformImpl?.ClientSize != size) + else PlatformImpl?.Resize(size, reason); } @@ -1186,7 +1233,7 @@ namespace Avalonia.Controls if (startupLocation == WindowStartupLocation.CenterOwner && (owner is null || - (owner is Window ownerWindow && ownerWindow.WindowState == WindowState.Minimized)) + (owner is Window ownerWindow && ownerWindow.EffectivePlatformWindowState == WindowState.Minimized)) ) { // If startup location is CenterOwner, but owner is null or minimized then fall back @@ -1344,9 +1391,6 @@ namespace Avalonia.Controls return result; } - - // HACK: Needs to fix maximize->normal transition, otherwise the layout pass will break window size - internal bool ResizeWindowInTopLevelHost => _canHandleResized && _isForcedDecorationMode; private protected sealed override Size ArrangeSetBounds(Size size) { @@ -1354,7 +1398,7 @@ namespace Avalonia.Controls // In forced decoration mode, TopLevelHost.ArrangeOverride handles the Resize call // because it knows the full frame size and can detect genuine content size changes // vs stale layout during window state transitions. - if (_canHandleResized && !_isForcedDecorationMode) + if (_canHandleResized) { ResizePlatformImpl(size, WindowResizeReason.Layout); } diff --git a/src/Avalonia.X11/X11Window.cs b/src/Avalonia.X11/X11Window.cs index bac88482ae..1ff3ab04f6 100644 --- a/src/Avalonia.X11/X11Window.cs +++ b/src/Avalonia.X11/X11Window.cs @@ -630,8 +630,9 @@ namespace Avalonia.X11 UpdateImePosition(); if (changedSize && !updatedSizeViaScaling && !_overrideRedirect) + { Resized?.Invoke(ClientSize, WindowResizeReason.Unspecified); - + } }, DispatcherPriority.AsyncRenderTargetResize); if (_useRenderWindow && !_useCompositorDrivenRenderWindowResize) @@ -752,7 +753,6 @@ namespace Avalonia.X11 { if(_lastWindowState == value) return; - _lastWindowState = value; if (value == WindowState.Minimized) { XIconifyWindow(_x11.Display, _handle, _x11.DefaultScreen); @@ -780,7 +780,6 @@ namespace Avalonia.X11 SendNetWMMessage(_x11.Atoms._NET_ACTIVE_WINDOW, (IntPtr)1, _x11.LastActivityTimestamp, IntPtr.Zero); } - WindowStateChanged?.Invoke(value); } } @@ -795,42 +794,40 @@ namespace Avalonia.X11 if (property == _x11.Atoms._NET_WM_STATE) { - WindowState state = WindowState.Normal; var atoms = hasValue ? XGetWindowPropertyAsIntPtrArray(_x11.Display, _handle, _x11.Atoms._NET_WM_STATE, (IntPtr)Atom.XA_ATOM) ?? [] : []; int maximized = 0; + bool hasMinimized = false, hasFullscreen = false; foreach (var atom in atoms) { - if (atom == _x11.Atoms._NET_WM_STATE_HIDDEN) - { - state = WindowState.Minimized; - break; - } - - if(atom == _x11.Atoms._NET_WM_STATE_FULLSCREEN) - { - state = WindowState.FullScreen; - break; - } + if (atom == _x11.Atoms._NET_WM_STATE_HIDDEN) + hasMinimized = true; if (atom == _x11.Atoms._NET_WM_STATE_MAXIMIZED_HORZ || - atom == _x11.Atoms._NET_WM_STATE_MAXIMIZED_VERT) - { + atom == _x11.Atoms._NET_WM_STATE_MAXIMIZED_VERT) maximized++; - if (maximized == 2) - { - state = WindowState.Maximized; - break; - } - } + + if(atom == _x11.Atoms._NET_WM_STATE_FULLSCREEN) + hasFullscreen = true; } + + var state = hasMinimized ? WindowState.Minimized + : hasFullscreen ? WindowState.FullScreen + : maximized == 2 ? WindowState.Maximized + : WindowState.Normal; + if (_lastWindowState != state) { _lastWindowState = state; WindowStateChanged?.Invoke(state); + + XGetGeometry(_x11.Display, _handle, out var _, out var _, out var _, out var width, out var height, + out var _, out var _); + _realSize = new(width, height); + Resized?.Invoke(ClientSize, WindowResizeReason.User); } _activationTracker?.OnNetWmStateChanged(atoms);