Browse Source

Less hacks but now we have a separate window state property

pull/20915/head
Nikita Tsukanov 23 hours ago
parent
commit
4f2ab8b724
  1. 18
      src/Avalonia.Controls/Chrome/WindowDrawnDecorations.cs
  2. 11
      src/Avalonia.Controls/TopLevelHost.cs
  3. 72
      src/Avalonia.Controls/Window.cs
  4. 43
      src/Avalonia.X11/X11Window.cs

18
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;
}

11
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

72
src/Avalonia.Controls/Window.cs

@ -157,11 +157,24 @@ namespace Avalonia.Controls
AvaloniaProperty.Register<Window, WindowClosingBehavior>(nameof(ClosingBehavior));
/// <summary>
/// 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.
/// </summary>
public static readonly StyledProperty<WindowState> WindowStateProperty =
AvaloniaProperty.Register<Window, WindowState>(nameof(WindowState));
private WindowState _effectivePlatformWindowStatePropertyCache;
/// <summary>
/// Represents the currently effective window state (normal, minimized, maximized)
/// </summary>
public static readonly DirectProperty<Window, WindowState> EffectivePlatformWindowStateProperty = AvaloniaProperty.RegisterDirect<Window, WindowState>(
"EffectivePlatformWindowState", o => o.EffectivePlatformWindowState);
/// <summary>
/// Defines the <see cref="Title"/> property.
/// </summary>
@ -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
}
/// <summary>
/// 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 <see cref="EffectivePlatformWindowState"/>
/// </summary>
public WindowState WindowState
{
get => GetValue(WindowStateProperty);
[Obsolete("Use TrySetWindowState")]
set => SetValue(WindowStateProperty, value);
}
/// <summary>
/// Represents the currently effective window state (normal, minimized, maximized)
/// </summary>
public WindowState EffectivePlatformWindowState => PlatformImpl?.WindowState ?? WindowState;
/// <summary>
/// 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);
}

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

Loading…
Cancel
Save