diff --git a/native/Avalonia.Native/src/OSX/window.h b/native/Avalonia.Native/src/OSX/window.h index e2f69c8359..3a54bd4b79 100644 --- a/native/Avalonia.Native/src/OSX/window.h +++ b/native/Avalonia.Native/src/OSX/window.h @@ -10,6 +10,8 @@ class WindowBaseImpl; -(void) setSwRenderedFrame: (AvnFramebuffer* _Nonnull) fb dispose: (IUnknown* _Nonnull) dispose; -(void) onClosed; -(AvnPixelSize) getPixelSize; +-(AvnPlatformResizeReason) getResizeReason; +-(void) setResizeReason:(AvnPlatformResizeReason)reason; @end @interface AutoFitContentView : NSView @@ -51,4 +53,23 @@ struct IWindowStateChanged virtual AvnWindowState WindowState () = 0; }; +class ResizeScope +{ +public: + ResizeScope(AvnView* _Nonnull view, AvnPlatformResizeReason reason) + { + _view = view; + _restore = [view getResizeReason]; + [view setResizeReason:reason]; + } + + ~ResizeScope() + { + [_view setResizeReason:_restore]; + } +private: + AvnView* _Nonnull _view; + AvnPlatformResizeReason _restore; +}; + #endif /* window_h */ diff --git a/native/Avalonia.Native/src/OSX/window.mm b/native/Avalonia.Native/src/OSX/window.mm index a64442b86c..9c6a0e6187 100644 --- a/native/Avalonia.Native/src/OSX/window.mm +++ b/native/Avalonia.Native/src/OSX/window.mm @@ -277,7 +277,7 @@ public: } } - virtual HRESULT Resize(double x, double y) override + virtual HRESULT Resize(double x, double y, AvnPlatformResizeReason reason) override { if(_inResize) { @@ -287,6 +287,7 @@ public: _inResize = true; START_COM_CALL; + auto resizeBlock = ResizeScope(View, reason); @autoreleasepool { @@ -317,7 +318,7 @@ public: { if(!_shown) { - BaseEvents->Resized(AvnSize{x,y}); + BaseEvents->Resized(AvnSize{x,y}, reason); } [Window setContentSize:NSSize{x, y}]; @@ -1385,6 +1386,7 @@ NSArray* AllLoopModes = [NSArray arrayWithObjects: NSDefaultRunLoopMode, NSEvent bool _lastKeyHandled; AvnPixelSize _lastPixelSize; NSObject* _renderTarget; + AvnPlatformResizeReason _resizeReason; } - (void)onClosed @@ -1496,7 +1498,8 @@ NSArray* AllLoopModes = [NSArray arrayWithObjects: NSDefaultRunLoopMode, NSEvent _lastPixelSize.Height = (int)fsize.height; [self updateRenderTarget]; - _parent->BaseEvents->Resized(AvnSize{newSize.width, newSize.height}); + auto reason = [self inLiveResize] ? ResizeUser : _resizeReason; + _parent->BaseEvents->Resized(AvnSize{newSize.width, newSize.height}, reason); } } @@ -1995,6 +1998,16 @@ NSArray* AllLoopModes = [NSArray arrayWithObjects: NSDefaultRunLoopMode, NSEvent } +- (AvnPlatformResizeReason)getResizeReason +{ + return _resizeReason; +} + +- (void)setResizeReason:(AvnPlatformResizeReason)reason +{ + _resizeReason = reason; +} + @end @@ -2382,7 +2395,7 @@ protected: return NSWindowStyleMaskBorderless; } - virtual HRESULT Resize(double x, double y) override + virtual HRESULT Resize(double x, double y, AvnPlatformResizeReason reason) override { START_COM_CALL; diff --git a/src/Android/Avalonia.Android/Platform/SkiaPlatform/TopLevelImpl.cs b/src/Android/Avalonia.Android/Platform/SkiaPlatform/TopLevelImpl.cs index a72742580c..0afb1db141 100644 --- a/src/Android/Avalonia.Android/Platform/SkiaPlatform/TopLevelImpl.cs +++ b/src/Android/Avalonia.Android/Platform/SkiaPlatform/TopLevelImpl.cs @@ -67,7 +67,7 @@ namespace Avalonia.Android.Platform.SkiaPlatform public Action Paint { get; set; } - public Action Resized { get; set; } + public Action Resized { get; set; } public Action ScalingChanged { get; set; } @@ -134,7 +134,7 @@ namespace Avalonia.Android.Platform.SkiaPlatform protected virtual void OnResized(Size size) { - Resized?.Invoke(size); + Resized?.Invoke(size, PlatformResizeReason.Unspecified); } class ViewImpl : InvalidationAwareSurfaceView, ISurfaceHolderCallback, IInitEditorInfo diff --git a/src/Avalonia.Controls/ApiCompatBaseline.txt b/src/Avalonia.Controls/ApiCompatBaseline.txt index 3e4fb7dfb4..fac5923db5 100644 --- a/src/Avalonia.Controls/ApiCompatBaseline.txt +++ b/src/Avalonia.Controls/ApiCompatBaseline.txt @@ -33,15 +33,26 @@ MembersMustExist : Member 'public Avalonia.AvaloniaProperty Avalonia.Controls.ApplicationLifetimes.IClassicDesktopStyleApplicationLifetime.ShutdownRequested' is present in the implementation but not in the contract. InterfacesShouldHaveSameMembers : Interface member 'public void Avalonia.Controls.ApplicationLifetimes.IClassicDesktopStyleApplicationLifetime.add_ShutdownRequested(System.EventHandler)' is present in the implementation but not in the contract. InterfacesShouldHaveSameMembers : Interface member 'public void Avalonia.Controls.ApplicationLifetimes.IClassicDesktopStyleApplicationLifetime.remove_ShutdownRequested(System.EventHandler)' is present in the implementation but not in the contract. +MembersMustExist : Member 'public System.Action Avalonia.Controls.Embedding.Offscreen.OffscreenTopLevelImplBase.Resized.get()' does not exist in the implementation but it does exist in the contract. +MembersMustExist : Member 'public void Avalonia.Controls.Embedding.Offscreen.OffscreenTopLevelImplBase.Resized.set(System.Action)' does not exist in the implementation but it does exist in the contract. MembersMustExist : Member 'public void Avalonia.Controls.Embedding.Offscreen.OffscreenTopLevelImplBase.SetCursor(Avalonia.Platform.IPlatformHandle)' does not exist in the implementation but it does exist in the contract. MembersMustExist : Member 'public Avalonia.AvaloniaProperty Avalonia.AvaloniaProperty Avalonia.Controls.Notifications.NotificationCard.CloseOnClickProperty' does not exist in the implementation but it does exist in the contract. EnumValuesMustMatch : Enum value 'Avalonia.Platform.ExtendClientAreaChromeHints Avalonia.Platform.ExtendClientAreaChromeHints.Default' is (System.Int32)2 in the implementation but (System.Int32)1 in the contract. InterfacesShouldHaveSameMembers : Interface member 'public System.Nullable Avalonia.Platform.ITopLevelImpl.FrameSize' is present in the implementation but not in the contract. InterfacesShouldHaveSameMembers : Interface member 'public System.Nullable Avalonia.Platform.ITopLevelImpl.FrameSize.get()' is present in the implementation but not in the contract. +InterfacesShouldHaveSameMembers : Interface member 'public System.Action Avalonia.Platform.ITopLevelImpl.Resized.get()' is present in the implementation but not in the contract. +InterfacesShouldHaveSameMembers : Interface member 'public System.Action Avalonia.Platform.ITopLevelImpl.Resized.get()' is present in the contract but not in the implementation. +MembersMustExist : Member 'public System.Action Avalonia.Platform.ITopLevelImpl.Resized.get()' does not exist in the implementation but it does exist in the contract. +InterfacesShouldHaveSameMembers : Interface member 'public void Avalonia.Platform.ITopLevelImpl.Resized.set(System.Action)' is present in the implementation but not in the contract. +InterfacesShouldHaveSameMembers : Interface member 'public void Avalonia.Platform.ITopLevelImpl.Resized.set(System.Action)' is present in the contract but not in the implementation. +MembersMustExist : Member 'public void Avalonia.Platform.ITopLevelImpl.Resized.set(System.Action)' does not exist in the implementation but it does exist in the contract. InterfacesShouldHaveSameMembers : Interface member 'public void Avalonia.Platform.ITopLevelImpl.SetCursor(Avalonia.Platform.ICursorImpl)' is present in the implementation but not in the contract. InterfacesShouldHaveSameMembers : Interface member 'public void Avalonia.Platform.ITopLevelImpl.SetCursor(Avalonia.Platform.IPlatformHandle)' is present in the contract but not in the implementation. MembersMustExist : Member 'public void Avalonia.Platform.ITopLevelImpl.SetCursor(Avalonia.Platform.IPlatformHandle)' does not exist in the implementation but it does exist in the contract. InterfacesShouldHaveSameMembers : Interface member 'public void Avalonia.Platform.IWindowBaseImpl.Show(System.Boolean)' is present in the contract but not in the implementation. MembersMustExist : Member 'public void Avalonia.Platform.IWindowBaseImpl.Show(System.Boolean)' does not exist in the implementation but it does exist in the contract. InterfacesShouldHaveSameMembers : Interface member 'public void Avalonia.Platform.IWindowBaseImpl.Show(System.Boolean, System.Boolean)' is present in the implementation but not in the contract. -Total Issues: 45 +InterfacesShouldHaveSameMembers : Interface member 'public void Avalonia.Platform.IWindowImpl.Resize(Avalonia.Size)' is present in the contract but not in the implementation. +MembersMustExist : Member 'public void Avalonia.Platform.IWindowImpl.Resize(Avalonia.Size)' does not exist in the implementation but it does exist in the contract. +InterfacesShouldHaveSameMembers : Interface member 'public void Avalonia.Platform.IWindowImpl.Resize(Avalonia.Size, Avalonia.Platform.PlatformResizeReason)' is present in the implementation but not in the contract. +Total Issues: 56 diff --git a/src/Avalonia.Controls/Embedding/Offscreen/OffscreenTopLevelImpl.cs b/src/Avalonia.Controls/Embedding/Offscreen/OffscreenTopLevelImpl.cs index 83470f161d..e2afbd3bdc 100644 --- a/src/Avalonia.Controls/Embedding/Offscreen/OffscreenTopLevelImpl.cs +++ b/src/Avalonia.Controls/Embedding/Offscreen/OffscreenTopLevelImpl.cs @@ -31,7 +31,7 @@ namespace Avalonia.Controls.Embedding.Offscreen set { _clientSize = value; - Resized?.Invoke(value); + Resized?.Invoke(value, PlatformResizeReason.Unspecified); } } @@ -49,7 +49,7 @@ namespace Avalonia.Controls.Embedding.Offscreen public Action Input { get; set; } public Action Paint { get; set; } - public Action Resized { get; set; } + public Action Resized { get; set; } public Action ScalingChanged { get; set; } public Action TransparencyLevelChanged { get; set; } diff --git a/src/Avalonia.Controls/Platform/ITopLevelImpl.cs b/src/Avalonia.Controls/Platform/ITopLevelImpl.cs index 6e53233898..f4f4d29168 100644 --- a/src/Avalonia.Controls/Platform/ITopLevelImpl.cs +++ b/src/Avalonia.Controls/Platform/ITopLevelImpl.cs @@ -3,11 +3,46 @@ using System.Collections.Generic; using Avalonia.Controls; using Avalonia.Input; using Avalonia.Input.Raw; +using Avalonia.Layout; using Avalonia.Rendering; using JetBrains.Annotations; namespace Avalonia.Platform { + /// + /// Describes the reason for a message. + /// + public enum PlatformResizeReason + { + /// + /// The resize reason is unknown or unspecified. + /// + Unspecified, + + /// + /// The resize was due to the user resizing the window, for example by dragging the + /// window frame. + /// + User, + + /// + /// The resize was initiated by the application, for example by setting one of the sizing- + /// related properties on such as or + /// . + /// + Application, + + /// + /// The resize was initiated by the layout system. + /// + Layout, + + /// + /// The resize was due to a change in DPI. + /// + DpiChange, + } + /// /// Defines a platform-specific top-level window implementation. /// @@ -57,7 +92,7 @@ namespace Avalonia.Platform /// /// Gets or sets a method called when the toplevel is resized. /// - Action Resized { get; set; } + Action Resized { get; set; } /// /// Gets or sets a method called when the toplevel's scaling changes. diff --git a/src/Avalonia.Controls/Platform/IWindowImpl.cs b/src/Avalonia.Controls/Platform/IWindowImpl.cs index 8a1554d344..17a90eddfe 100644 --- a/src/Avalonia.Controls/Platform/IWindowImpl.cs +++ b/src/Avalonia.Controls/Platform/IWindowImpl.cs @@ -110,7 +110,9 @@ namespace Avalonia.Platform /// /// Sets the client size of the top level. /// - void Resize(Size clientSize); + /// The new client size. + /// The reason for the resize. + void Resize(Size clientSize, PlatformResizeReason reason = PlatformResizeReason.Application); /// /// Sets the client size of the top level. diff --git a/src/Avalonia.Controls/Primitives/PopupRoot.cs b/src/Avalonia.Controls/Primitives/PopupRoot.cs index da7352b77f..1a11778db2 100644 --- a/src/Avalonia.Controls/Primitives/PopupRoot.cs +++ b/src/Avalonia.Controls/Primitives/PopupRoot.cs @@ -161,12 +161,9 @@ namespace Avalonia.Controls.Primitives protected override sealed Size ArrangeSetBounds(Size size) { - using (BeginAutoSizing()) - { - _positionerParameters.Size = size; - UpdatePosition(); - return ClientSize; - } + _positionerParameters.Size = size; + UpdatePosition(); + return ClientSize; } } } diff --git a/src/Avalonia.Controls/TopLevel.cs b/src/Avalonia.Controls/TopLevel.cs index 7028dca769..116e276a3a 100644 --- a/src/Avalonia.Controls/TopLevel.cs +++ b/src/Avalonia.Controls/TopLevel.cs @@ -376,11 +376,15 @@ namespace Avalonia.Controls LayoutManager?.Dispose(); } + [Obsolete("Use HandleResized(Size, PlatformResizeReason)")] + protected virtual void HandleResized(Size clientSize) => HandleResized(clientSize, PlatformResizeReason.Unspecified); + /// /// Handles a resize notification from . /// /// The new client size. - protected virtual void HandleResized(Size clientSize) + /// The reason for the resize. + protected virtual void HandleResized(Size clientSize, PlatformResizeReason reason) { ClientSize = clientSize; FrameSize = PlatformImpl.FrameSize; diff --git a/src/Avalonia.Controls/Window.cs b/src/Avalonia.Controls/Window.cs index 5c5b97b211..0ca28ca196 100644 --- a/src/Avalonia.Controls/Window.cs +++ b/src/Avalonia.Controls/Window.cs @@ -244,7 +244,7 @@ namespace Avalonia.Controls impl.WindowStateChanged = HandleWindowStateChanged; _maxPlatformClientSize = PlatformImpl?.MaxAutoSizeHint ?? default(Size); impl.ExtendClientAreaToDecorationsChanged = ExtendClientAreaToDecorationsChanged; - this.GetObservable(ClientSizeProperty).Skip(1).Subscribe(x => PlatformImpl?.Resize(x)); + this.GetObservable(ClientSizeProperty).Skip(1).Subscribe(x => PlatformImpl?.Resize(x, PlatformResizeReason.Application)); PlatformImpl?.ShowTaskbarIcon(ShowInTaskbar); } @@ -258,6 +258,18 @@ namespace Avalonia.Controls /// /// Gets or sets a value indicating how the window will size itself to fit its content. /// + /// + /// If has a value other than , + /// is automatically set to + /// if a user resizes the window by using the resize grip or dragging the border. + /// + /// NOTE: Because of a limitation of X11, will be reset on X11 to + /// on any resize - including the resize that happens when + /// the window is first shown. This is because X11 resize notifications are asynchronous and + /// there is no way to know whether a resize came from the user or the layout system. To avoid + /// this, consider setting to false, which will disable user resizing + /// of the window. + /// public SizeToContent SizeToContent { get { return GetValue(SizeToContentProperty); } @@ -583,28 +595,23 @@ namespace Avalonia.Controls return; } - using (BeginAutoSizing()) - { - Renderer?.Stop(); + Renderer?.Stop(); - if (Owner is Window owner) - { - owner.RemoveChild(this); - } + if (Owner is Window owner) + { + owner.RemoveChild(this); + } - if (_children.Count > 0) + if (_children.Count > 0) + { + foreach (var child in _children.ToArray()) { - foreach (var child in _children.ToArray()) - { - child.child.Hide(); - } + child.child.Hide(); } - - Owner = null; - - PlatformImpl?.Hide(); } + Owner = null; + PlatformImpl?.Hide(); IsVisible = false; } @@ -675,29 +682,23 @@ namespace Avalonia.Controls if (initialSize != ClientSize) { - using (BeginAutoSizing()) - { - PlatformImpl?.Resize(initialSize); - } + PlatformImpl?.Resize(initialSize, PlatformResizeReason.Layout); } LayoutManager.ExecuteInitialLayoutPass(); - using (BeginAutoSizing()) + if (parent != null) { - if (parent != null) - { - PlatformImpl?.SetParent(parent.PlatformImpl); - } - - Owner = parent; - parent?.AddChild(this, false); - - SetWindowStartupLocation(Owner?.PlatformImpl); - - PlatformImpl?.Show(ShowActivated, false); - Renderer?.Start(); + PlatformImpl?.SetParent(parent.PlatformImpl); } + + Owner = parent; + parent?.AddChild(this, false); + + SetWindowStartupLocation(Owner?.PlatformImpl); + + PlatformImpl?.Show(ShowActivated, false); + Renderer?.Start(); OnOpened(EventArgs.Empty); } @@ -760,41 +761,34 @@ namespace Avalonia.Controls if (initialSize != ClientSize) { - using (BeginAutoSizing()) - { - PlatformImpl?.Resize(initialSize); - } + PlatformImpl?.Resize(initialSize, PlatformResizeReason.Layout); } LayoutManager.ExecuteInitialLayoutPass(); var result = new TaskCompletionSource(); - using (BeginAutoSizing()) - { - PlatformImpl?.SetParent(owner.PlatformImpl); - Owner = owner; - owner.AddChild(this, true); - - SetWindowStartupLocation(owner.PlatformImpl); - - PlatformImpl?.Show(ShowActivated, true); + PlatformImpl?.SetParent(owner.PlatformImpl); + Owner = owner; + owner.AddChild(this, true); - Renderer?.Start(); + SetWindowStartupLocation(owner.PlatformImpl); - Observable.FromEventPattern( - x => Closed += x, - x => Closed -= x) - .Take(1) - .Subscribe(_ => - { - owner.Activate(); - result.SetResult((TResult)(_dialogResult ?? default(TResult))); - }); + PlatformImpl?.Show(ShowActivated, true); - OnOpened(EventArgs.Empty); - } + Renderer?.Start(); + + Observable.FromEventPattern( + x => Closed += x, + x => Closed -= x) + .Take(1) + .Subscribe(_ => + { + owner.Activate(); + result.SetResult((TResult)(_dialogResult ?? default(TResult))); + }); + OnOpened(EventArgs.Empty); return result.Task; } @@ -937,11 +931,8 @@ namespace Avalonia.Controls protected sealed override Size ArrangeSetBounds(Size size) { - using (BeginAutoSizing()) - { - PlatformImpl?.Resize(size); - return ClientSize; - } + PlatformImpl?.Resize(size, PlatformResizeReason.Layout); + return ClientSize; } protected sealed override void HandleClosed() @@ -958,18 +949,36 @@ namespace Avalonia.Controls Owner = null; } + [Obsolete("Use HandleResized(Size, PlatformResizeReason)")] + protected sealed override void HandleResized(Size clientSize) => HandleResized(clientSize, PlatformResizeReason.Unspecified); + /// - protected sealed override void HandleResized(Size clientSize) + protected sealed override void HandleResized(Size clientSize, PlatformResizeReason reason) { - if (!AutoSizing) + if (ClientSize == clientSize) + return; + + var sizeToContent = SizeToContent; + + // If auto-sizing is enabled, and the resize came from a user resize (or the reason was + // unspecified) then turn off auto-resizing for any window dimension that is not equal + // to the requested size. + if (sizeToContent != SizeToContent.Manual && + CanResize && + reason == PlatformResizeReason.Unspecified || + reason == PlatformResizeReason.User) { - SizeToContent = SizeToContent.Manual; + if (clientSize.Width != ClientSize.Width) + sizeToContent &= ~SizeToContent.Width; + if (clientSize.Height != ClientSize.Height) + sizeToContent &= ~SizeToContent.Height; + SizeToContent = sizeToContent; } Width = clientSize.Width; Height = clientSize.Height; - base.HandleResized(clientSize); + base.HandleResized(clientSize, reason); } /// diff --git a/src/Avalonia.Controls/WindowBase.cs b/src/Avalonia.Controls/WindowBase.cs index e4396edb44..7cfc5f8be8 100644 --- a/src/Avalonia.Controls/WindowBase.cs +++ b/src/Avalonia.Controls/WindowBase.cs @@ -39,7 +39,6 @@ namespace Avalonia.Controls public static readonly StyledProperty TopmostProperty = AvaloniaProperty.Register(nameof(Topmost)); - private int _autoSizing; private bool _hasExecutedInitialLayoutPass; private bool _isActive; private bool _ignoreVisibilityChange; @@ -95,10 +94,8 @@ namespace Avalonia.Controls public Screens Screens { get; private set; } - /// - /// Whether an auto-size operation is in progress. - /// - protected bool AutoSizing => _autoSizing > 0; + [Obsolete("No longer used. Always returns false.")] + protected bool AutoSizing => false; /// /// Gets or sets the owner of the window. @@ -172,20 +169,9 @@ namespace Avalonia.Controls } } - /// - /// Begins an auto-resize operation. - /// - /// A disposable used to finish the operation. - /// - /// When an auto-resize operation is in progress any resize events received will not be - /// cause the new size to be written to the and - /// properties. - /// - protected IDisposable BeginAutoSizing() - { - ++_autoSizing; - return Disposable.Create(() => --_autoSizing); - } + + [Obsolete("No longer used. Has no effect.")] + protected IDisposable BeginAutoSizing() => Disposable.Empty; /// /// Ensures that the window is initialized. @@ -215,11 +201,15 @@ namespace Avalonia.Controls } } + [Obsolete("Use HandleResized(Size, PlatformResizeReason)")] + protected override void HandleResized(Size clientSize) => HandleResized(clientSize, PlatformResizeReason.Unspecified); + /// /// Handles a resize notification from . /// /// The new client size. - protected override void HandleResized(Size clientSize) + /// The reason for the resize. + protected override void HandleResized(Size clientSize, PlatformResizeReason reason) { ClientSize = clientSize; FrameSize = PlatformImpl.FrameSize; diff --git a/src/Avalonia.DesignerSupport/Remote/PreviewerWindowImpl.cs b/src/Avalonia.DesignerSupport/Remote/PreviewerWindowImpl.cs index e82ec0fb0f..5cae29cafd 100644 --- a/src/Avalonia.DesignerSupport/Remote/PreviewerWindowImpl.cs +++ b/src/Avalonia.DesignerSupport/Remote/PreviewerWindowImpl.cs @@ -58,7 +58,7 @@ namespace Avalonia.DesignerSupport.Remote base.OnMessage(transport, obj); } - public void Resize(Size clientSize) + public void Resize(Size clientSize, PlatformResizeReason reason) { _transport.Send(new RequestViewportResizeMessage { diff --git a/src/Avalonia.DesignerSupport/Remote/Stubs.cs b/src/Avalonia.DesignerSupport/Remote/Stubs.cs index fbfefe84d7..9dcd4d8e87 100644 --- a/src/Avalonia.DesignerSupport/Remote/Stubs.cs +++ b/src/Avalonia.DesignerSupport/Remote/Stubs.cs @@ -27,7 +27,7 @@ namespace Avalonia.DesignerSupport.Remote public IEnumerable Surfaces { get; } public Action Input { get; set; } public Action Paint { get; set; } - public Action Resized { get; set; } + public Action Resized { get; set; } public Action ScalingChanged { get; set; } public Func Closing { get; set; } public Action Closed { get; set; } @@ -54,7 +54,7 @@ namespace Avalonia.DesignerSupport.Remote PopupPositioner = new ManagedPopupPositioner(new ManagedPopupPositionerPopupImplHelper(parent, (_, size, __) => { - Resize(size); + Resize(size, PlatformResizeReason.Unspecified); })); } @@ -98,7 +98,7 @@ namespace Avalonia.DesignerSupport.Remote { } - public void Resize(Size clientSize) + public void Resize(Size clientSize, PlatformResizeReason reason) { } diff --git a/src/Avalonia.Headless/HeadlessWindowImpl.cs b/src/Avalonia.Headless/HeadlessWindowImpl.cs index 901e2f933d..31315848d1 100644 --- a/src/Avalonia.Headless/HeadlessWindowImpl.cs +++ b/src/Avalonia.Headless/HeadlessWindowImpl.cs @@ -47,7 +47,7 @@ namespace Avalonia.Headless public IEnumerable Surfaces { get; } public Action Input { get; set; } public Action Paint { get; set; } - public Action Resized { get; set; } + public Action Resized { get; set; } public Action ScalingChanged { get; set; } public IRenderer CreateRenderer(IRenderRoot root) @@ -108,7 +108,7 @@ namespace Avalonia.Headless public Action Activated { get; set; } public IPlatformHandle Handle { get; } = new PlatformHandle(IntPtr.Zero, "STUB"); public Size MaxClientSize { get; } = new Size(1920, 1280); - public void Resize(Size clientSize) + public void Resize(Size clientSize, PlatformResizeReason reason) { // Emulate X11 behavior here if (IsPopup) @@ -126,7 +126,7 @@ namespace Avalonia.Headless if (ClientSize != clientSize) { ClientSize = clientSize; - Resized?.Invoke(clientSize); + Resized?.Invoke(clientSize, PlatformResizeReason.Unspecified); } } diff --git a/src/Avalonia.Native/PopupImpl.cs b/src/Avalonia.Native/PopupImpl.cs index 8740dd6f12..4680a2af17 100644 --- a/src/Avalonia.Native/PopupImpl.cs +++ b/src/Avalonia.Native/PopupImpl.cs @@ -32,7 +32,7 @@ namespace Avalonia.Native private void MoveResize(PixelPoint position, Size size, double scaling) { Position = position; - Resize(size); + Resize(size, PlatformResizeReason.Layout); //TODO: We ignore the scaling override for now } diff --git a/src/Avalonia.Native/WindowImplBase.cs b/src/Avalonia.Native/WindowImplBase.cs index 3c9e9b0c88..4a3baa2788 100644 --- a/src/Avalonia.Native/WindowImplBase.cs +++ b/src/Avalonia.Native/WindowImplBase.cs @@ -87,7 +87,7 @@ namespace Avalonia.Native var monitor = Screen.AllScreens.OrderBy(x => x.PixelDensity) .FirstOrDefault(m => m.Bounds.Contains(Position)); - Resize(new Size(monitor.WorkingArea.Width * 0.75d, monitor.WorkingArea.Height * 0.7d)); + Resize(new Size(monitor.WorkingArea.Width * 0.75d, monitor.WorkingArea.Height * 0.7d), PlatformResizeReason.Layout); } public Size ClientSize @@ -146,7 +146,7 @@ namespace Avalonia.Native public Action LostFocus { get; set; } public Action Paint { get; set; } - public Action Resized { get; set; } + public Action Resized { get; set; } public Action Closed { get; set; } public IMouseDevice MouseDevice => _mouse; public abstract IPopupImpl CreatePopup(); @@ -186,13 +186,13 @@ namespace Avalonia.Native _parent.Paint?.Invoke(new Rect(0, 0, s.Width, s.Height)); } - void IAvnWindowBaseEvents.Resized(AvnSize* size) + void IAvnWindowBaseEvents.Resized(AvnSize* size, AvnPlatformResizeReason reason) { if (_parent?._native != null) { var s = new Size(size->Width, size->Height); _parent._savedLogicalSize = s; - _parent.Resized?.Invoke(s); + _parent.Resized?.Invoke(s, (PlatformResizeReason)reason); } } @@ -320,9 +320,9 @@ namespace Avalonia.Native } } - public void Resize(Size clientSize) + public void Resize(Size clientSize, PlatformResizeReason reason) { - _native.Resize(clientSize.Width, clientSize.Height); + _native.Resize(clientSize.Width, clientSize.Height, (AvnPlatformResizeReason)reason); } public IRenderer CreateRenderer(IRenderRoot root) diff --git a/src/Avalonia.Native/avn.idl b/src/Avalonia.Native/avn.idl index e0dfbffb11..70d85dacdd 100644 --- a/src/Avalonia.Native/avn.idl +++ b/src/Avalonia.Native/avn.idl @@ -400,6 +400,15 @@ enum AvnExtendClientAreaChromeHints AvnDefaultChrome = AvnPreferSystemChrome, } +enum AvnPlatformResizeReason +{ + ResizeUnspecified, + ResizeUser, + ResizeApplication, + ResizeLayout, + ResizeDpiChange, +} + [uuid(809c652e-7396-11d2-9771-00a0c9b4d50c)] interface IAvaloniaNativeFactory : IUnknown { @@ -438,7 +447,7 @@ interface IAvnWindowBase : IUnknown HRESULT GetFrameSize(AvnSize*ret); HRESULT GetScaling(double*ret); HRESULT SetMinMaxSize(AvnSize minSize, AvnSize maxSize); - HRESULT Resize(double width, double height); + HRESULT Resize(double width, double height, AvnPlatformResizeReason reason); HRESULT Invalidate(AvnRect rect); HRESULT BeginMoveDrag(); HRESULT BeginResizeDrag(AvnWindowEdge edge); @@ -492,7 +501,7 @@ interface IAvnWindowBaseEvents : IUnknown void Closed(); void Activated(); void Deactivated(); - void Resized([const] AvnSize& size); + void Resized([const] AvnSize& size, AvnPlatformResizeReason reason); void PositionChanged(AvnPoint position); void RawMouseEvent(AvnRawMouseEventType type, uint timeStamp, diff --git a/src/Avalonia.X11/X11Window.cs b/src/Avalonia.X11/X11Window.cs index 7977e3226e..149d7fb9b2 100644 --- a/src/Avalonia.X11/X11Window.cs +++ b/src/Avalonia.X11/X11Window.cs @@ -336,7 +336,7 @@ namespace Avalonia.X11 public IEnumerable Surfaces { get; } public Action Input { get; set; } public Action Paint { get; set; } - public Action Resized { get; set; } + public Action Resized { get; set; } //TODO public Action ScalingChanged { get; set; } public Action Deactivated { get; set; } @@ -510,7 +510,7 @@ namespace Avalonia.X11 UpdateImePosition(); if (changedSize && !updatedSizeViaScaling && !_popup) - Resized?.Invoke(ClientSize); + Resized?.Invoke(ClientSize, PlatformResizeReason.Unspecified); Dispatcher.UIThread.RunJobs(DispatcherPriority.Layout); }, DispatcherPriority.Layout); @@ -565,7 +565,7 @@ namespace Avalonia.X11 UpdateImePosition(); SetMinMaxSize(_scaledMinMaxSize.minSize, _scaledMinMaxSize.maxSize); if(!skipResize) - Resize(oldScaledSize, true); + Resize(oldScaledSize, true, PlatformResizeReason.DpiChange); return true; } @@ -616,7 +616,7 @@ namespace Avalonia.X11 { // Occurs once the window has been mapped, which is the earliest the extents // can be retrieved, so invoke event to force update of TopLevel.FrameSize. - Resized.Invoke(ClientSize); + Resized.Invoke(ClientSize, PlatformResizeReason.Unspecified); } if (atom == _x11.Atoms._NET_WM_STATE) @@ -862,19 +862,19 @@ namespace Avalonia.X11 } - public void Resize(Size clientSize) => Resize(clientSize, false); + public void Resize(Size clientSize, PlatformResizeReason reason) => Resize(clientSize, false, reason); public void Move(PixelPoint point) => Position = point; private void MoveResize(PixelPoint position, Size size, double scaling) { Move(position); _scalingOverride = scaling; UpdateScaling(true); - Resize(size, true); + Resize(size, true, PlatformResizeReason.Layout); } PixelSize ToPixelSize(Size size) => new PixelSize((int)(size.Width * RenderScaling), (int)(size.Height * RenderScaling)); - void Resize(Size clientSize, bool force) + void Resize(Size clientSize, bool force, PlatformResizeReason reason) { if (!force && clientSize == ClientSize) return; @@ -891,7 +891,7 @@ namespace Avalonia.X11 if (force || !_wasMappedAtLeastOnce || (_popup && needImmediatePopupResize)) { _realSize = pixelSize; - Resized?.Invoke(ClientSize); + Resized?.Invoke(ClientSize, reason); } } diff --git a/src/Linux/Avalonia.LinuxFramebuffer/FramebufferToplevelImpl.cs b/src/Linux/Avalonia.LinuxFramebuffer/FramebufferToplevelImpl.cs index 5a1da9058a..b097e0917f 100644 --- a/src/Linux/Avalonia.LinuxFramebuffer/FramebufferToplevelImpl.cs +++ b/src/Linux/Avalonia.LinuxFramebuffer/FramebufferToplevelImpl.cs @@ -69,7 +69,7 @@ namespace Avalonia.LinuxFramebuffer public IEnumerable Surfaces { get; } public Action Input { get; set; } public Action Paint { get; set; } - public Action Resized { get; set; } + public Action Resized { get; set; } public Action ScalingChanged { get; set; } public Action TransparencyLevelChanged { get; set; } diff --git a/src/Windows/Avalonia.Win32.Interop/Wpf/WpfTopLevelImpl.cs b/src/Windows/Avalonia.Win32.Interop/Wpf/WpfTopLevelImpl.cs index 73e46b9e13..d36db107e3 100644 --- a/src/Windows/Avalonia.Win32.Interop/Wpf/WpfTopLevelImpl.cs +++ b/src/Windows/Avalonia.Win32.Interop/Wpf/WpfTopLevelImpl.cs @@ -44,7 +44,7 @@ namespace Avalonia.Win32.Interop.Wpf ((FrameworkElement)PlatformImpl)?.InvalidateMeasure(); } - protected override void HandleResized(Size clientSize) + protected override void HandleResized(Size clientSize, PlatformResizeReason reason) { ClientSize = clientSize; LayoutManager.ExecuteLayoutPass(); @@ -114,7 +114,7 @@ namespace Avalonia.Win32.Interop.Wpf if (_finalSize == _previousSize) return finalSize; _previousSize = _finalSize; - _ttl.Resized?.Invoke(finalSize.ToAvaloniaSize()); + _ttl.Resized?.Invoke(finalSize.ToAvaloniaSize(), PlatformResizeReason.Unspecified); return base.ArrangeOverride(finalSize); } @@ -236,7 +236,7 @@ namespace Avalonia.Win32.Interop.Wpf Action ITopLevelImpl.Input { get; set; } //TODO Action ITopLevelImpl.Paint { get; set; } - Action ITopLevelImpl.Resized { get; set; } + Action ITopLevelImpl.Resized { get; set; } Action ITopLevelImpl.ScalingChanged { get; set; } Action ITopLevelImpl.TransparencyLevelChanged { get; set; } diff --git a/src/Windows/Avalonia.Win32/PopupImpl.cs b/src/Windows/Avalonia.Win32/PopupImpl.cs index 7b2d20a986..38dc4a16dc 100644 --- a/src/Windows/Avalonia.Win32/PopupImpl.cs +++ b/src/Windows/Avalonia.Win32/PopupImpl.cs @@ -135,7 +135,7 @@ namespace Avalonia.Win32 private void MoveResize(PixelPoint position, Size size, double scaling) { Move(position); - Resize(size); + Resize(size, PlatformResizeReason.Layout); //TODO: We ignore the scaling override for now } diff --git a/src/Windows/Avalonia.Win32/WindowImpl.AppWndProc.cs b/src/Windows/Avalonia.Win32/WindowImpl.AppWndProc.cs index 8e755a33bc..d163b3d068 100644 --- a/src/Windows/Avalonia.Win32/WindowImpl.AppWndProc.cs +++ b/src/Windows/Avalonia.Win32/WindowImpl.AppWndProc.cs @@ -2,9 +2,10 @@ using System.Diagnostics.CodeAnalysis; using System.Runtime.InteropServices; using Avalonia.Controls; -using Avalonia.Controls.Platform; +using Avalonia.Controls.Remote; using Avalonia.Input; using Avalonia.Input.Raw; +using Avalonia.Platform; using Avalonia.Win32.Input; using static Avalonia.Win32.Interop.UnmanagedMethods; @@ -18,7 +19,6 @@ namespace Avalonia.Win32 { const double wheelDelta = 120.0; uint timestamp = unchecked((uint)GetMessageTime()); - RawInputEventArgs e = null; var shouldTakeFocus = false; @@ -94,14 +94,19 @@ namespace Avalonia.Win32 var newDisplayRect = Marshal.PtrToStructure(lParam); _scaling = dpi / 96.0; ScalingChanged?.Invoke(_scaling); - SetWindowPos(hWnd, - IntPtr.Zero, - newDisplayRect.left, - newDisplayRect.top, - newDisplayRect.right - newDisplayRect.left, - newDisplayRect.bottom - newDisplayRect.top, - SetWindowPosFlags.SWP_NOZORDER | - SetWindowPosFlags.SWP_NOACTIVATE); + + using (SetResizeReason(PlatformResizeReason.DpiChange)) + { + SetWindowPos(hWnd, + IntPtr.Zero, + newDisplayRect.left, + newDisplayRect.top, + newDisplayRect.right - newDisplayRect.left, + newDisplayRect.bottom - newDisplayRect.top, + SetWindowPosFlags.SWP_NOZORDER | + SetWindowPosFlags.SWP_NOACTIVATE); + } + return IntPtr.Zero; } @@ -364,6 +369,11 @@ namespace Avalonia.Win32 return IntPtr.Zero; } + + case WindowsMessage.WM_ENTERSIZEMOVE: + _resizeReason = PlatformResizeReason.User; + break; + case WindowsMessage.WM_SIZE: { using(NonPumpingSyncContext.Use()) @@ -379,7 +389,7 @@ namespace Avalonia.Win32 size == SizeCommand.Maximized)) { var clientSize = new Size(ToInt32(lParam) & 0xffff, ToInt32(lParam) >> 16); - Resized(clientSize / RenderScaling); + Resized(clientSize / RenderScaling, _resizeReason); } var windowState = size == SizeCommand.Maximized ? @@ -403,6 +413,10 @@ namespace Avalonia.Win32 return IntPtr.Zero; } + case WindowsMessage.WM_EXITSIZEMOVE: + _resizeReason = PlatformResizeReason.Unspecified; + break; + case WindowsMessage.WM_MOVE: { PositionChanged?.Invoke(new PixelPoint((short)(ToInt32(lParam) & 0xffff), diff --git a/src/Windows/Avalonia.Win32/WindowImpl.cs b/src/Windows/Avalonia.Win32/WindowImpl.cs index 77d5bb6e28..f20dae3e50 100644 --- a/src/Windows/Avalonia.Win32/WindowImpl.cs +++ b/src/Windows/Avalonia.Win32/WindowImpl.cs @@ -53,6 +53,7 @@ namespace Avalonia.Win32 private double _extendTitleBarHint = -1; private bool _isUsingComposition; private IBlurHost _blurHost; + private PlatformResizeReason _resizeReason; #if USE_MANAGED_DRAG private readonly ManagedWindowResizeDragHelper _managedDrag; @@ -160,7 +161,7 @@ namespace Avalonia.Win32 public Action Paint { get; set; } - public Action Resized { get; set; } + public Action Resized { get; set; } public Action ScalingChanged { get; set; } @@ -482,7 +483,7 @@ namespace Avalonia.Win32 : new ImmediateRenderer(root); } - public void Resize(Size value) + public void Resize(Size value, PlatformResizeReason reason) { int requestedClientWidth = (int)(value.Width * RenderScaling); int requestedClientHeight = (int)(value.Height * RenderScaling); @@ -494,6 +495,7 @@ namespace Avalonia.Win32 { GetWindowRect(_hwnd, out var windowRect); + using var scope = SetResizeReason(reason); SetWindowPos( _hwnd, IntPtr.Zero, @@ -930,7 +932,7 @@ namespace Avalonia.Win32 _offScreenMargin = new Thickness(); _extendedMargins = new Thickness(); - Resize(new Size(rcWindow.Width/ RenderScaling, rcWindow.Height / RenderScaling)); + Resize(new Size(rcWindow.Width/ RenderScaling, rcWindow.Height / RenderScaling), PlatformResizeReason.Layout); } if(!_isClientAreaExtended || (_extendChromeHints.HasAllFlags(ExtendClientAreaChromeHints.SystemChrome) && @@ -1311,6 +1313,13 @@ namespace Avalonia.Win32 /// public AcrylicPlatformCompensationLevels AcrylicCompensationLevels { get; } = new AcrylicPlatformCompensationLevels(1, 0.8, 0); + private ResizeReasonScope SetResizeReason(PlatformResizeReason reason) + { + var old = _resizeReason; + _resizeReason = reason; + return new ResizeReasonScope(this, old); + } + private struct SavedWindowInfo { public WindowStyles Style { get; set; } @@ -1325,5 +1334,19 @@ namespace Avalonia.Win32 public SystemDecorations Decorations; public bool IsFullScreen; } + + private struct ResizeReasonScope : IDisposable + { + private readonly WindowImpl _owner; + private readonly PlatformResizeReason _restore; + + public ResizeReasonScope(WindowImpl owner, PlatformResizeReason restore) + { + _owner = owner; + _restore = restore; + } + + public void Dispose() => _owner._resizeReason = _restore; + } } } diff --git a/src/iOS/Avalonia.iOS/AvaloniaView.cs b/src/iOS/Avalonia.iOS/AvaloniaView.cs index 0371a7759a..5bb2f64879 100644 --- a/src/iOS/Avalonia.iOS/AvaloniaView.cs +++ b/src/iOS/Avalonia.iOS/AvaloniaView.cs @@ -96,7 +96,7 @@ namespace Avalonia.iOS public IEnumerable Surfaces { get; set; } public Action Input { get; set; } public Action Paint { get; set; } - public Action Resized { get; set; } + public Action Resized { get; set; } public Action ScalingChanged { get; set; } public Action TransparencyLevelChanged { get; set; } public Action Closed { get; set; } @@ -127,7 +127,7 @@ namespace Avalonia.iOS public override void LayoutSubviews() { - _topLevelImpl.Resized?.Invoke(_topLevelImpl.ClientSize); + _topLevelImpl.Resized?.Invoke(_topLevelImpl.ClientSize, PlatformResizeReason.Layout); base.LayoutSubviews(); } diff --git a/tests/Avalonia.Controls.UnitTests/TopLevelTests.cs b/tests/Avalonia.Controls.UnitTests/TopLevelTests.cs index 6b30aed257..9c2d760733 100644 --- a/tests/Avalonia.Controls.UnitTests/TopLevelTests.cs +++ b/tests/Avalonia.Controls.UnitTests/TopLevelTests.cs @@ -139,7 +139,7 @@ namespace Avalonia.Controls.UnitTests // The user has resized the window, so we can no longer auto-size. var target = new TestTopLevel(impl.Object); - impl.Object.Resized(new Size(100, 200)); + impl.Object.Resized(new Size(100, 200), PlatformResizeReason.Unspecified); Assert.Equal(100, target.Width); Assert.Equal(200, target.Height); diff --git a/tests/Avalonia.Controls.UnitTests/WindowTests.cs b/tests/Avalonia.Controls.UnitTests/WindowTests.cs index 6b9921d83d..eb128ef038 100644 --- a/tests/Avalonia.Controls.UnitTests/WindowTests.cs +++ b/tests/Avalonia.Controls.UnitTests/WindowTests.cs @@ -663,10 +663,11 @@ namespace Avalonia.Controls.UnitTests var clientSize = new Size(200, 200); var maxClientSize = new Size(480, 480); - windowImpl.Setup(x => x.Resize(It.IsAny())).Callback(size => + windowImpl.Setup(x => x.Resize(It.IsAny(), It.IsAny())) + .Callback((size, reason) => { clientSize = size.Constrain(maxClientSize); - windowImpl.Object.Resized?.Invoke(clientSize); + windowImpl.Object.Resized?.Invoke(clientSize, reason); }); windowImpl.Setup(x => x.ClientSize).Returns(() => clientSize); @@ -739,6 +740,36 @@ namespace Avalonia.Controls.UnitTests } } + [Fact] + public void SizeToContent_Should_Not_Be_Lost_On_Scaling_Change() + { + using (UnitTestApplication.Start(TestServices.StyledWindow)) + { + var child = new Canvas + { + Width = 209, + Height = 117, + }; + + var target = new Window() + { + SizeToContent = SizeToContent.WidthAndHeight, + Content = child + }; + + Show(target); + + // Size before and after DPI change is a real-world example, with size after DPI + // change coming from Win32 WM_DPICHANGED. + target.PlatformImpl.ScalingChanged(1.5); + target.PlatformImpl.Resized( + new Size(210.66666666666666, 118.66666666666667), + PlatformResizeReason.DpiChange); + + Assert.Equal(SizeToContent.WidthAndHeight, target.SizeToContent); + } + } + [Fact] public void Width_Height_Should_Be_Updated_When_SizeToContent_Is_WidthAndHeight() { @@ -791,8 +822,91 @@ namespace Avalonia.Controls.UnitTests target.LayoutManager.ExecuteLayoutPass(); var windowImpl = Mock.Get(target.PlatformImpl); - windowImpl.Verify(x => x.Resize(new Size(410, 800))); + windowImpl.Verify(x => x.Resize(new Size(410, 800), PlatformResizeReason.Application)); + Assert.Equal(410, target.Width); + } + } + + + [Fact] + public void User_Resize_Of_Window_Width_Should_Reset_SizeToContent() + { + using (UnitTestApplication.Start(TestServices.StyledWindow)) + { + var target = new Window() + { + SizeToContent = SizeToContent.WidthAndHeight, + Content = new Canvas + { + Width = 400, + Height = 800, + }, + }; + + Show(target); + Assert.Equal(400, target.Width); + Assert.Equal(800, target.Height); + + target.PlatformImpl.Resized(new Size(410, 800), PlatformResizeReason.User); + Assert.Equal(410, target.Width); + Assert.Equal(800, target.Height); + Assert.Equal(SizeToContent.Height, target.SizeToContent); + } + } + + [Fact] + public void User_Resize_Of_Window_Height_Should_Reset_SizeToContent() + { + using (UnitTestApplication.Start(TestServices.StyledWindow)) + { + var target = new Window() + { + SizeToContent = SizeToContent.WidthAndHeight, + Content = new Canvas + { + Width = 400, + Height = 800, + }, + }; + + Show(target); + Assert.Equal(400, target.Width); + Assert.Equal(800, target.Height); + + target.PlatformImpl.Resized(new Size(400, 810), PlatformResizeReason.User); + + Assert.Equal(400, target.Width); + Assert.Equal(810, target.Height); + Assert.Equal(SizeToContent.Width, target.SizeToContent); + } + } + + [Fact] + public void Window_Resize_Should_Not_Reset_SizeToContent_If_CanResize_False() + { + using (UnitTestApplication.Start(TestServices.StyledWindow)) + { + var target = new Window() + { + SizeToContent = SizeToContent.WidthAndHeight, + CanResize = false, + Content = new Canvas + { + Width = 400, + Height = 800, + }, + }; + + Show(target); + Assert.Equal(400, target.Width); + Assert.Equal(800, target.Height); + + target.PlatformImpl.Resized(new Size(410, 810), PlatformResizeReason.Unspecified); + + Assert.Equal(400, target.Width); + Assert.Equal(800, target.Height); + Assert.Equal(SizeToContent.WidthAndHeight, target.SizeToContent); } } diff --git a/tests/Avalonia.UnitTests/MockWindowingPlatform.cs b/tests/Avalonia.UnitTests/MockWindowingPlatform.cs index 713283aa2b..bc003537f4 100644 --- a/tests/Avalonia.UnitTests/MockWindowingPlatform.cs +++ b/tests/Avalonia.UnitTests/MockWindowingPlatform.cs @@ -52,10 +52,11 @@ namespace Avalonia.UnitTests windowImpl.Object.PositionChanged?.Invoke(x); }); - windowImpl.Setup(x => x.Resize(It.IsAny())).Callback(x => + windowImpl.Setup(x => x.Resize(It.IsAny(), It.IsAny())) + .Callback((x, y) => { clientSize = x.Constrain(s_screenSize); - windowImpl.Object.Resized?.Invoke(clientSize); + windowImpl.Object.Resized?.Invoke(clientSize, y); }); windowImpl.Setup(x => x.Show(true, It.IsAny())).Callback(() => @@ -75,7 +76,7 @@ namespace Avalonia.UnitTests { clientSize = size.Constrain(s_screenSize); popupImpl.Object.PositionChanged?.Invoke(pos); - popupImpl.Object.Resized?.Invoke(clientSize); + popupImpl.Object.Resized?.Invoke(clientSize, PlatformResizeReason.Unspecified); }); var positioner = new ManagedPopupPositioner(positionerHelper);