Bläddra i källkod

Add Window.CanMinimize/CanMaximize (#18117)

* Add CanMinimize and CanMaximize to Window

* Win32 impl for CanMinimize/CanMaximize

* Add CanResize/CanMinimize/CanMaximize samples to control catalog

* X11 impl for CanMinimize/CanMaximize

* macOS impl for CanMinimize/CanMaximize

* Win32: don't allow restore when the window isn't resizable

* Additional documentation for CanMinimize/CanMaximize

* Add CanMinimize/CanMaximize logic to CaptionButtons

* Use START_COM_ARP_CALL

* Added CanMinimize/CanMaximize integration tests

* Fixed CanMaximize tests on macOS
Julien Lebosquain 2 månader sedan
förälder
incheckning
1f8701a2fc

+ 7 - 1
native/Avalonia.Native/src/OSX/WindowImpl.h

@@ -45,6 +45,10 @@ BEGIN_INTERFACE_MAP()
     void DoZoom();
 
     virtual HRESULT SetCanResize(bool value) override;
+    
+    virtual HRESULT SetCanMinimize(bool value) override;
+    
+    virtual HRESULT SetCanMaximize(bool value) override;
 
     virtual HRESULT SetDecorations(SystemDecorations value) override;
 
@@ -82,7 +86,7 @@ BEGIN_INTERFACE_MAP()
     
     bool CanBecomeKeyWindow ();
 
-    bool CanZoom() override { return _isEnabled && _canResize; }
+    bool CanZoom() override { return _isEnabled && _canMaximize; }
     
 protected:
     virtual NSWindowStyleMask CalculateStyleMask() override;
@@ -94,6 +98,8 @@ private:
     NSString *_lastTitle;
     bool _isEnabled;
     bool _canResize;
+    bool _canMinimize;
+    bool _canMaximize;
     bool _fullScreenActive;
     SystemDecorations _decorations;
     AvnWindowState _lastWindowState;

+ 23 - 4
native/Avalonia.Native/src/OSX/WindowImpl.mm

@@ -16,6 +16,8 @@ WindowImpl::WindowImpl(IAvnWindowEvents *events) : TopLevelImpl(events), WindowB
     _extendClientHints = AvnDefaultChrome;
     _fullScreenActive = false;
     _canResize = true;
+    _canMinimize = true;
+    _canMaximize = true;
     _decorations = SystemDecorationsFull;
     _transitioningWindowState = false;
     _inSetWindowState = false;
@@ -191,7 +193,8 @@ bool WindowImpl::IsZoomed() {
 void WindowImpl::DoZoom() {
     if (_decorations == SystemDecorationsNone ||
         _decorations == SystemDecorationsBorderOnly ||
-        _canResize == false) {
+        _canResize == false ||
+        _canMaximize == false) {
         [Window setFrame:[Window screen].visibleFrame display:true];
     } else {
         [Window performZoom:Window];
@@ -208,6 +211,22 @@ HRESULT WindowImpl::SetCanResize(bool value) {
     }
 }
 
+HRESULT WindowImpl::SetCanMinimize(bool value) {
+    START_COM_ARP_CALL;
+
+    _canMinimize = value;
+    UpdateAppearance();
+    return S_OK;
+}
+
+HRESULT WindowImpl::SetCanMaximize(bool value) {
+    START_COM_ARP_CALL;
+
+    _canMaximize = value;
+    UpdateAppearance();
+    return S_OK;
+}
+
 HRESULT WindowImpl::SetDecorations(SystemDecorations value) {
     START_COM_CALL;
 
@@ -583,7 +602,7 @@ NSWindowStyleMask WindowImpl::CalculateStyleMask() {
             break;
     }
 
-    if (!IsOwned()) {
+    if (_canMinimize && !IsOwned()) {
         s |= NSWindowStyleMaskMiniaturizable;
     }
 
@@ -611,9 +630,9 @@ void WindowImpl::UpdateAppearance() {
     [closeButton setHidden:!hasTrafficLights];
     [closeButton setEnabled:_isEnabled];
     [miniaturizeButton setHidden:!hasTrafficLights];
-    [miniaturizeButton setEnabled:_isEnabled];
+    [miniaturizeButton setEnabled:_isEnabled && _canMinimize];
     [zoomButton setHidden:!hasTrafficLights];
-    [zoomButton setEnabled:CanZoom()];
+    [zoomButton setEnabled:CanZoom() || (([Window styleMask] & NSWindowStyleMaskFullScreen) != 0 && _isEnabled)];
 }
 
 extern IAvnWindow* CreateAvnWindow(IAvnWindowEvents*events)

+ 3 - 0
samples/ControlCatalog/MainWindow.xaml

@@ -10,6 +10,9 @@
         ExtendClientAreaToDecorationsHint="{Binding ExtendClientAreaEnabled}"
         ExtendClientAreaChromeHints="{Binding ChromeHints}"
         ExtendClientAreaTitleBarHeightHint="{Binding TitleBarHeight}"
+        CanResize="{Binding CanResize}"
+        CanMinimize="{Binding CanMinimize}"
+        CanMaximize="{Binding CanMaximize}"
         x:Name="MainWindow"
         Background="Transparent"
         x:Class="ControlCatalog.MainWindow" WindowState="{Binding WindowState, Mode=TwoWay}"

+ 43 - 6
samples/ControlCatalog/Pages/WindowCustomizationsPage.xaml

@@ -8,18 +8,55 @@
              x:DataType="viewModels:MainWindowViewModel"
              x:CompileBindings="True">
   <StackPanel>
-    <StackPanel Spacing="10" Margin="25" IsEnabled="{OnFormFactor false, Desktop=true}">
-      <TextBlock Classes="h2" Text="Desktop properties" Margin="4" />
-      <CheckBox Content="Extend Client Area to Decorations" IsChecked="{Binding ExtendClientAreaEnabled}" />
-      <CheckBox Content="Title Bar" IsChecked="{Binding SystemTitleBarEnabled}" />
-      <CheckBox Content="Prefer System Chrome" IsChecked="{Binding PreferSystemChromeEnabled}" />
-      <Slider Minimum="-1" Maximum="200" Value="{Binding TitleBarHeight}" />
+
+    <StackPanel
+      Spacing="10"
+      Margin="25"
+      IsEnabled="{OnFormFactor false, Desktop=true}">
+
+      <TextBlock Classes="h2"
+                 Text="Desktop properties"
+                 Margin="4" />
+
+      <CheckBox Content="Extend Client Area to Decorations"
+                IsChecked="{Binding ExtendClientAreaEnabled}" />
+
+      <DockPanel IsEnabled="{Binding ExtendClientAreaEnabled}">
+
+        <CheckBox Content="Title Bar"
+                  IsChecked="{Binding SystemTitleBarEnabled}"
+                  DockPanel.Dock="Left" />
+
+        <Slider Minimum="-1"
+                Maximum="200"
+                Value="{Binding TitleBarHeight}"
+                IsEnabled="{Binding SystemTitleBarEnabled}"
+                Margin="8,-10" />
+
+      </DockPanel>
+
+      <CheckBox Content="Prefer System Chrome"
+                IsChecked="{Binding PreferSystemChromeEnabled}"
+                IsEnabled="{Binding ExtendClientAreaEnabled}" />
+
+      <CheckBox Content="Can Resize"
+                IsChecked="{Binding CanResize}" />
+
+      <CheckBox Content="Can Minimize"
+                IsChecked="{Binding CanMinimize}" />
+
+      <CheckBox Content="Can Maximize"
+                IsChecked="{Binding CanMaximize}"
+                IsEnabled="{Binding CanResize}" />
+
     </StackPanel>
+
     <StackPanel Spacing="10" Margin="25" IsEnabled="{OnFormFactor false, Mobile=true}">
       <TextBlock Classes="h2" Text="Mobile properties" Margin="4" />
       <CheckBox Content="Is System Bar Visible" IsChecked="{Binding IsSystemBarVisible}" />
       <CheckBox Content="Display Edge To Edge" IsChecked="{Binding DisplayEdgeToEdge}" />
       <TextBlock Text="{Binding SafeAreaPadding, StringFormat='Safe Area Padding: {0}'}" />
     </StackPanel>
+
   </StackPanel>
 </UserControl>

+ 50 - 14
samples/ControlCatalog/ViewModels/MainWindowViewModel.cs

@@ -1,6 +1,5 @@
 using Avalonia.Controls;
 using Avalonia.Controls.ApplicationLifetimes;
-using Avalonia.Controls.Notifications;
 using Avalonia.Dialogs;
 using Avalonia.Platform;
 using System;
@@ -22,6 +21,9 @@ namespace ControlCatalog.ViewModels
         private bool _isSystemBarVisible;
         private bool _displayEdgeToEdge;
         private Thickness _safeAreaPadding;
+        private bool _canResize;
+        private bool _canMinimize;
+        private bool _canMaximize;
 
         public MainWindowViewModel()
         {
@@ -49,7 +51,7 @@ namespace ControlCatalog.ViewModels
                 WindowState.FullScreen,
             };
 
-            this.PropertyChanged += (s, e) =>
+            PropertyChanged += (s, e) =>
                 {
                     if (e.PropertyName is nameof(SystemTitleBarEnabled) or nameof(PreferSystemChromeEnabled))
                     {
@@ -67,70 +69,104 @@ namespace ControlCatalog.ViewModels
                     }
                 };
 
-            SystemTitleBarEnabled = true;            
+            SystemTitleBarEnabled = true;
             TitleBarHeight = -1;
+            CanResize = true;
+            CanMinimize = true;
+            CanMaximize = true;
         }        
         
         public ExtendClientAreaChromeHints ChromeHints
         {
             get { return _chromeHints; }
-            set { this.RaiseAndSetIfChanged(ref _chromeHints, value); }
+            set { RaiseAndSetIfChanged(ref _chromeHints, value); }
         }
 
         public bool ExtendClientAreaEnabled
         {
             get { return _extendClientAreaEnabled; }
-            set { this.RaiseAndSetIfChanged(ref _extendClientAreaEnabled, value); }
+            set
+            {
+                if (RaiseAndSetIfChanged(ref _extendClientAreaEnabled, value) && !value)
+                {
+                    SystemTitleBarEnabled = true;
+                }
+            }
         }
 
         public bool SystemTitleBarEnabled
         {
             get { return _systemTitleBarEnabled; }
-            set { this.RaiseAndSetIfChanged(ref _systemTitleBarEnabled, value); }
+            set
+            {
+                if (RaiseAndSetIfChanged(ref _systemTitleBarEnabled, value) && !value)
+                {
+                    TitleBarHeight = -1;
+                }
+            }
         }
 
         public bool PreferSystemChromeEnabled
         {
             get { return _preferSystemChromeEnabled; }
-            set { this.RaiseAndSetIfChanged(ref _preferSystemChromeEnabled, value); }
+            set { RaiseAndSetIfChanged(ref _preferSystemChromeEnabled, value); }
         }
 
         public double TitleBarHeight
         {
             get { return _titleBarHeight; }
-            set { this.RaiseAndSetIfChanged(ref _titleBarHeight, value); }
+            set { RaiseAndSetIfChanged(ref _titleBarHeight, value); }
         }
 
         public WindowState WindowState
         {
             get { return _windowState; }
-            set { this.RaiseAndSetIfChanged(ref _windowState, value); }
+            set { RaiseAndSetIfChanged(ref _windowState, value); }
         }
 
         public WindowState[] WindowStates
         {
             get { return _windowStates; }
-            set { this.RaiseAndSetIfChanged(ref _windowStates, value); }
+            set { RaiseAndSetIfChanged(ref _windowStates, value); }
         }
 
         public bool IsSystemBarVisible
         {
             get { return _isSystemBarVisible; }
-            set { this.RaiseAndSetIfChanged(ref _isSystemBarVisible, value); }
+            set { RaiseAndSetIfChanged(ref _isSystemBarVisible, value); }
         }
 
         public bool DisplayEdgeToEdge
         {
             get { return _displayEdgeToEdge; }
-            set { this.RaiseAndSetIfChanged(ref _displayEdgeToEdge, value); }
+            set { RaiseAndSetIfChanged(ref _displayEdgeToEdge, value); }
         }
         
         public Thickness SafeAreaPadding
         {
             get { return _safeAreaPadding; }
-            set { this.RaiseAndSetIfChanged(ref _safeAreaPadding, value); }
+            set { RaiseAndSetIfChanged(ref _safeAreaPadding, value); }
+        }
+
+        public bool CanResize
+        {
+            get { return _canResize; }
+            set { RaiseAndSetIfChanged(ref _canResize, value); }
+        }
+
+        public bool CanMinimize
+        {
+            get { return _canMinimize; }
+            set { RaiseAndSetIfChanged(ref _canMinimize, value); }
         }
 
+        public bool CanMaximize
+        {
+            get { return _canMaximize; }
+            set { RaiseAndSetIfChanged(ref _canMaximize, value); }
+        }
+
+
         public MiniCommand AboutCommand { get; }
 
         public MiniCommand ExitCommand { get; }
@@ -144,7 +180,7 @@ namespace ControlCatalog.ViewModels
         public DateTime? ValidatedDateExample
         {
             get => _validatedDateExample;
-            set => this.RaiseAndSetIfChanged(ref _validatedDateExample, value);
+            set => RaiseAndSetIfChanged(ref _validatedDateExample, value);
         }
     }
 }

+ 4 - 2
samples/IntegrationTestApp/Pages/WindowPage.axaml

@@ -30,14 +30,16 @@
       </ComboBox>
       <CheckBox Name="ShowWindowExtendClientAreaToDecorationsHint">ExtendClientAreaToDecorationsHint</CheckBox>
       <CheckBox Name="ShowWindowCanResize" IsChecked="True">Can Resize</CheckBox>
+      <CheckBox Name="ShowWindowCanMinimize" IsChecked="True">Can Minimize</CheckBox>
+      <CheckBox Name="ShowWindowCanMaximize" IsChecked="True">Can Maximize</CheckBox>
+    </StackPanel>
+    <StackPanel Grid.Column="2">
       <Button Name="ShowWindow" Click="ShowWindow_Click">Show Window</Button>
       <Button Name="SendToBack" Click="SendToBack_Click">Send to Back</Button>
       <Button Name="EnterFullscreen" Click="EnterFullscreen_Click">Enter Fullscreen</Button>
       <Button Name="ExitFullscreen" Click="ExitFullscreen_Click">Exit Fullscreen</Button>
       <Button Name="RestoreAll" Click="RestoreAll_Click">Restore All</Button>
       <Button Name="ShowTopmostWindow" Click="ShowTopmostWindow_Click">Show Topmost Window</Button>
-    </StackPanel>
-    <StackPanel Grid.Column="2">
       <Button Name="ShowTransparentWindow" Click="ShowTransparentWindow_Click">Transparent Window</Button>
       <Button Name="ShowTransparentPopup" Click="ShowTransparentPopup_Click">Transparent Popup</Button>
     </StackPanel>

+ 4 - 1
samples/IntegrationTestApp/Pages/WindowPage.axaml.cs

@@ -23,10 +23,13 @@ public partial class WindowPage : UserControl
     private void ShowWindow_Click(object? sender, RoutedEventArgs e)
     {
         var size = !string.IsNullOrWhiteSpace(ShowWindowSize.Text) ? Size.Parse(ShowWindowSize.Text) : (Size?)null;
+        var canResize = ShowWindowCanResize.IsChecked ?? false;
         var window = new ShowWindowTest
         {
             WindowStartupLocation = (WindowStartupLocation)ShowWindowLocation.SelectedIndex,
-            CanResize = ShowWindowCanResize.IsChecked ?? false,
+            CanResize = canResize,
+            CanMinimize = ShowWindowCanMinimize.IsChecked ?? false,
+            CanMaximize = canResize && (ShowWindowCanMaximize.IsChecked ?? false)
         };
 
         if (Application.Current?.ApplicationLifetime is IClassicDesktopStyleApplicationLifetime lifetime)

+ 50 - 5
src/Avalonia.Controls/Chrome/CaptionButtons.cs

@@ -21,6 +21,8 @@ namespace Avalonia.Controls.Chrome
         internal const string PART_FullScreenButton = "PART_FullScreenButton";
 
         private Button? _restoreButton;
+        private Button? _minimizeButton;
+        private Button? _fullScreenButton;
         private IDisposable? _disposables;
 
         /// <summary>
@@ -36,11 +38,16 @@ namespace Avalonia.Controls.Chrome
 
                 _disposables = new CompositeDisposable
                 {
-                    HostWindow.GetObservable(Window.CanResizeProperty)
-                        .Subscribe(x =>
+                    HostWindow.GetObservable(Window.CanMaximizeProperty)
+                        .Subscribe(_ =>
+                        {
+                            UpdateRestoreButtonState();
+                            UpdateFullScreenButtonState();
+                        }),
+                    HostWindow.GetObservable(Window.CanMinimizeProperty)
+                        .Subscribe(_ =>
                         {
-                            if (_restoreButton is not null)
-                                _restoreButton.IsEnabled = x;
+                            UpdateMinimizeButtonState();
                         }),
                     HostWindow.GetObservable(Window.WindowStateProperty)
                         .Subscribe(x =>
@@ -49,6 +56,9 @@ namespace Avalonia.Controls.Chrome
                             PseudoClasses.Set(":normal", x == WindowState.Normal);
                             PseudoClasses.Set(":maximized", x == WindowState.Maximized);
                             PseudoClasses.Set(":fullscreen", x == WindowState.FullScreen);
+                            UpdateRestoreButtonState();
+                            UpdateMinimizeButtonState();
+                            UpdateFullScreenButtonState();
                         }),
                 };
             }
@@ -116,8 +126,8 @@ namespace Avalonia.Controls.Chrome
                     OnRestore();
                     args.Handled = true;
                 };
-                restoreButton.IsEnabled = HostWindow?.CanResize ?? true;
                 _restoreButton = restoreButton;
+                UpdateRestoreButtonState();
             }
             
             if (e.NameScope.Find<Button>(PART_MinimizeButton) is { } minimizeButton)
@@ -127,6 +137,8 @@ namespace Avalonia.Controls.Chrome
                     OnMinimize();
                     args.Handled = true;
                 };
+                _minimizeButton = minimizeButton;
+                UpdateMinimizeButtonState();
             }
             
             if (e.NameScope.Find<Button>(PART_FullScreenButton) is { } fullScreenButton)
@@ -136,7 +148,40 @@ namespace Avalonia.Controls.Chrome
                     OnToggleFullScreen();
                     args.Handled = true;
                 };
+                _fullScreenButton = fullScreenButton;
+                UpdateFullScreenButtonState();
             }
         }
+
+        private void UpdateRestoreButtonState()
+        {
+            if (_restoreButton is null)
+                return;
+
+            _restoreButton.IsEnabled = HostWindow?.WindowState switch
+            {
+                WindowState.Maximized or WindowState.FullScreen => HostWindow.CanResize,
+                WindowState.Normal => HostWindow.CanMaximize,
+                _ => true
+            };
+        }
+
+        private void UpdateMinimizeButtonState()
+        {
+            if (_minimizeButton is null)
+                return;
+
+            _minimizeButton.IsEnabled = HostWindow?.CanMinimize ?? true;
+        }
+
+        private void UpdateFullScreenButtonState()
+        {
+            if (_fullScreenButton is null)
+                return;
+
+            _fullScreenButton.IsEnabled = HostWindow?.WindowState == WindowState.FullScreen ?
+                HostWindow.CanResize :
+                HostWindow?.CanMaximize ?? true;
+        }
     }
 }

+ 10 - 1
src/Avalonia.Controls/Platform/IWindowImpl.cs

@@ -64,6 +64,16 @@ namespace Avalonia.Platform
         /// </summary>
         void CanResize(bool value);
 
+        /// <summary>
+        /// Enables or disables minimizing the window.
+        /// </summary>
+        void SetCanMinimize(bool value);
+
+        /// <summary>
+        /// Enables or disables maximizing the window.
+        /// </summary>
+        void SetCanMaximize(bool value);
+
         /// <summary>
         /// Gets or sets a method called before the underlying implementation is destroyed.
         /// Return true to prevent the underlying implementation from closing.
@@ -124,7 +134,6 @@ namespace Avalonia.Platform
         /// <summary>
         /// Minimum width of the window.
         /// </summary>
-        /// 
         void SetMinMaxSize(Size minSize, Size maxSize);
 
         /// <summary>

+ 52 - 1
src/Avalonia.Controls/Window.cs

@@ -181,9 +181,24 @@ namespace Avalonia.Controls
         public static readonly StyledProperty<WindowStartupLocation> WindowStartupLocationProperty =
             AvaloniaProperty.Register<Window, WindowStartupLocation>(nameof(WindowStartupLocation));
 
+        /// <summary>
+        /// Defines the <see cref="CanResize"/> property.
+        /// </summary>
         public static readonly StyledProperty<bool> CanResizeProperty =
             AvaloniaProperty.Register<Window, bool>(nameof(CanResize), true);
 
+        /// <summary>
+        /// Defines the <see cref="CanMinimize"/> property.
+        /// </summary>
+        public static readonly StyledProperty<bool> CanMinimizeProperty =
+            AvaloniaProperty.Register<Window, bool>(nameof(CanMinimize), true);
+
+        /// <summary>
+        /// Defines the <see cref="CanMaximize"/> property.
+        /// </summary>
+        public static readonly StyledProperty<bool> CanMaximizeProperty =
+            AvaloniaProperty.Register<Window, bool>(nameof(CanMaximize), true, coerce: CoerceCanMaximize);
+
         /// <summary>
         /// Routed event that can be used for global tracking of window destruction
         /// </summary>
@@ -236,6 +251,8 @@ namespace Avalonia.Controls
             CreatePlatformImplBinding(TitleProperty, title => PlatformImpl!.SetTitle(title));
             CreatePlatformImplBinding(IconProperty, icon => PlatformImpl!.SetIcon((icon ?? s_defaultIcon.Value)?.PlatformImpl));
             CreatePlatformImplBinding(CanResizeProperty, canResize => PlatformImpl!.CanResize(canResize));
+            CreatePlatformImplBinding(CanMinimizeProperty, canMinimize => PlatformImpl!.SetCanMinimize(canMinimize));
+            CreatePlatformImplBinding(CanMaximizeProperty, canMaximize => PlatformImpl!.SetCanMaximize(canMaximize));
             CreatePlatformImplBinding(ShowInTaskbarProperty, show => PlatformImpl!.ShowTaskbarIcon(show));
 
             CreatePlatformImplBinding(WindowStateProperty, state => PlatformImpl!.WindowState = state);
@@ -407,6 +424,32 @@ namespace Avalonia.Controls
             set => SetValue(CanResizeProperty, value);
         }
 
+        /// <summary>
+        /// Enables or disables minimizing the window.
+        /// </summary>
+        /// <remarks>
+        /// This property might be ignored by some window managers on Linux.
+        /// </remarks>
+        public bool CanMinimize
+        {
+            get => GetValue(CanMinimizeProperty);
+            set => SetValue(CanMinimizeProperty, value);
+        }
+
+        /// <summary>
+        /// Enables or disables maximizing the window.
+        /// </summary>
+        /// <remarks>
+        /// <para>When <see cref="CanResize"/> is false, this property is always false.</para>
+        /// <para>On macOS, setting this property to false also disables the full screen mode.</para>
+        /// <para>This property might be ignored by some window managers on Linux.</para>
+        /// </remarks>
+        public bool CanMaximize
+        {
+            get => GetValue(CanMaximizeProperty);
+            set => SetValue(CanMaximizeProperty, value);
+        }
+
         /// <summary>
         /// Gets or sets the icon of the window.
         /// </summary>
@@ -1184,7 +1227,7 @@ namespace Avalonia.Controls
                 PlatformImpl?.SetSystemDecorations(typedNewValue);
             }
 
-            if (change.Property == OwnerProperty)
+            else if (change.Property == OwnerProperty)
             {
                 var oldParent = change.OldValue as Window;
                 var newParent = change.NewValue as Window;
@@ -1197,6 +1240,11 @@ namespace Avalonia.Controls
                     impl.SetParent(_showingAsDialog ? newParent?.PlatformImpl! : (newParent?.PlatformImpl ?? null));
                 }
             }
+
+            else if (change.Property == CanResizeProperty)
+            {
+                CoerceValue(CanMaximizeProperty);
+            }
         }
 
         protected override AutomationPeer OnCreateAutomationPeer()
@@ -1217,5 +1265,8 @@ namespace Avalonia.Controls
             }
             return null;
         }
+
+        private static bool CoerceCanMaximize(AvaloniaObject target, bool value)
+            => value && target is not Window { CanResize: false };
     }
 }

+ 8 - 0
src/Avalonia.DesignerSupport/Remote/PreviewerWindowImpl.cs

@@ -133,6 +133,14 @@ namespace Avalonia.DesignerSupport.Remote
         {
         }
 
+        public void SetCanMinimize(bool value)
+        {
+        }
+
+        public void SetCanMaximize(bool value)
+        {
+        }
+
         public void SetTopmost(bool value)
         {
         }

+ 8 - 0
src/Avalonia.DesignerSupport/Remote/Stubs.cs

@@ -152,6 +152,14 @@ namespace Avalonia.DesignerSupport.Remote
         {
         }
 
+        public void SetCanMinimize(bool value)
+        {
+        }
+
+        public void SetCanMaximize(bool value)
+        {
+        }
+
         public void SetTopmost(bool value)
         {
         }

+ 22 - 3
src/Avalonia.Native/WindowImpl.cs

@@ -18,6 +18,7 @@ namespace Avalonia.Native
         private DoubleClickHelper _doubleClickHelper;
         private readonly ITopLevelNativeMenuExporter _nativeMenuExporter;
         private bool _canResize = true;
+        private bool _canMaximize = true;
 
         internal WindowImpl(IAvaloniaNativeFactory factory, AvaloniaNativePlatformOptions opts) : base(factory)
         {
@@ -77,6 +78,17 @@ namespace Avalonia.Native
             _native.SetCanResize(value.AsComBool());
         }
 
+        public void SetCanMinimize(bool value)
+        {
+            _native.SetCanMinimize(value.AsComBool());
+        }
+
+        public void SetCanMaximize(bool value)
+        {
+            _canMaximize = value;
+            _native.SetCanMaximize(value.AsComBool());
+        }
+
         public void SetSystemDecorations(Controls.SystemDecorations enabled)
         {
             _native.SetDecorations((Interop.SystemDecorations)enabled);
@@ -138,10 +150,17 @@ namespace Avalonia.Native
                     {
                         if (_doubleClickHelper.IsDoubleClick(e.Timestamp, e.Position))
                         {
-                            if (_canResize)
+                            switch (WindowState)
                             {
-                                WindowState = WindowState is WindowState.Maximized or WindowState.FullScreen ?
-                                    WindowState.Normal : WindowState.Maximized;
+                                case WindowState.Maximized or WindowState.FullScreen
+                                when _canResize:
+                                    WindowState = WindowState.Normal;
+                                    break;
+
+                                case WindowState.Normal
+                                when _canMaximize:
+                                    WindowState = WindowState.Maximized;
+                                    break;
                             }
                         }
                         else

+ 2 - 0
src/Avalonia.Native/avn.idl

@@ -772,6 +772,8 @@ interface IAvnWindow : IAvnWindowBase
 {
      HRESULT SetEnabled(bool enable);
      HRESULT SetCanResize(bool value);
+     HRESULT SetCanMinimize(bool value);
+     HRESULT SetCanMaximize(bool value);
      HRESULT SetDecorations(SystemDecorations value);
      HRESULT SetTitle(char* utf8Title);
      HRESULT SetTitleBarColor(AvnColor color);

+ 1 - 1
src/Avalonia.Themes.Fluent/Controls/CaptionButtons.xaml

@@ -112,7 +112,7 @@
     <Style Selector="^:fullscreen /template/ Button#PART_MinimizeButton">
       <Setter Property="IsVisible" Value="False" />
     </Style>
-    <Style Selector="^ /template/ Button#PART_RestoreButton:disabled">
+    <Style Selector="^ /template/ Button:disabled">
       <Setter Property="Opacity" Value="0.2"/>
     </Style>
   </ControlTheme>

+ 29 - 9
src/Avalonia.X11/X11Window.cs

@@ -316,22 +316,28 @@ namespace Avalonia.X11
                 || _systemDecorations == SystemDecorations.None) 
                 decorations = 0;
 
-            if (!_canResize || !IsEnabled)
+            var isDisabled = !IsEnabled;
+
+            if (!_canResize || isDisabled)
             {
-                functions &= ~(MotifFunctions.Resize | MotifFunctions.Maximize);
-                decorations &= ~(MotifDecorations.Maximize | MotifDecorations.ResizeH);
+                functions &= ~MotifFunctions.Resize;
+                decorations &= ~MotifDecorations.ResizeH;
             }
-            if (!IsEnabled)
-            {
-                functions &= ~(MotifFunctions.Resize | MotifFunctions.Minimize);
 
-                UpdateSizeHints(null, true);
+            if (!_canMinimize || isDisabled)
+            {
+                functions &= ~MotifFunctions.Minimize;
+                decorations &= ~MotifDecorations.Minimize;
             }
-            else
+
+            if (!_canMaximize || isDisabled)
             {
-                UpdateSizeHints(null);
+                functions &= ~MotifFunctions.Maximize;
+                decorations &= ~MotifDecorations.Maximize;
             }
 
+            UpdateSizeHints(null, isDisabled);
+
             var hints = new MotifWmHints
             {
                 flags = new IntPtr((int)(MotifFlags.Decorations | MotifFlags.Functions)),
@@ -857,6 +863,8 @@ namespace Avalonia.X11
         
         private SystemDecorations _systemDecorations = SystemDecorations.Full;
         private bool _canResize = true;
+        private bool _canMinimize = true;
+        private bool _canMaximize = true;
         private const int MaxWindowDimension = 100000;
 
         private (Size minSize, Size maxSize) _scaledMinMaxSize =
@@ -1162,6 +1170,18 @@ namespace Avalonia.X11
             UpdateSizeHints(null);
         }
 
+        public void SetCanMinimize(bool value)
+        {
+            _canMinimize = value;
+            UpdateMotifHints();
+        }
+
+        public void SetCanMaximize(bool value)
+        {
+            _canMaximize = value;
+            UpdateMotifHints();
+        }
+
         public void SetCursor(ICursorImpl? cursor)
         {
             if (cursor == null)

+ 8 - 0
src/Headless/Avalonia.Headless/HeadlessWindowImpl.cs

@@ -165,6 +165,14 @@ namespace Avalonia.Headless
 
         }
 
+        public void SetCanMinimize(bool value)
+        {
+        }
+
+        public void SetCanMaximize(bool value)
+        {
+        }
+
         public Func<WindowCloseReason, bool>? Closing { get; set; }
 
         private class FramebufferProxy : ILockedFramebuffer

+ 2 - 0
src/Windows/Avalonia.Win32/EmbeddedWindowImpl.cs

@@ -12,6 +12,8 @@ namespace Avalonia.Win32
             {
                 ShowInTaskbar = false,
                 IsResizable = false,
+                IsMinimizable = false,
+                IsMaximizable = false,
                 Decorations = SystemDecorations.None
             };
         }

+ 2 - 0
src/Windows/Avalonia.Win32/PopupImpl.cs

@@ -111,6 +111,8 @@ namespace Avalonia.Win32
             {
                 ShowInTaskbar = false,
                 IsResizable = false,
+                IsMinimizable = false,
+                IsMaximizable = false,
                 Decorations = SystemDecorations.None,
             };
 

+ 31 - 5
src/Windows/Avalonia.Win32/WindowImpl.cs

@@ -136,6 +136,8 @@ namespace Avalonia.Win32
             {
                 ShowInTaskbar = false,
                 IsResizable = true,
+                IsMinimizable = true,
+                IsMaximizable = true,
                 Decorations = SystemDecorations.Full
             };
 
@@ -858,6 +860,24 @@ namespace Avalonia.Win32
             UpdateWindowProperties(newWindowProperties);
         }
 
+        public void SetCanMinimize(bool value)
+        {
+            var newWindowProperties = _windowProperties;
+
+            newWindowProperties.IsMinimizable = value;
+
+            UpdateWindowProperties(newWindowProperties);
+        }
+
+        public void SetCanMaximize(bool value)
+        {
+            var newWindowProperties = _windowProperties;
+
+            newWindowProperties.IsMaximizable = value;
+
+            UpdateWindowProperties(newWindowProperties);
+        }
+
         public void SetSystemDecorations(SystemDecorations value)
         {
             var newWindowProperties = _windowProperties;
@@ -1425,15 +1445,19 @@ namespace Avalonia.Win32
                     style |= WindowStyles.WS_VISIBLE;
 
                 if (newProperties.IsResizable || newProperties.WindowState == WindowState.Maximized)
-                {
                     style |= WindowStyles.WS_THICKFRAME;
-                    style |= WindowStyles.WS_MAXIMIZEBOX;
-                }
                 else
-                {
                     style &= ~WindowStyles.WS_THICKFRAME;
+
+                if (newProperties.IsMinimizable)
+                    style |= WindowStyles.WS_MINIMIZEBOX;
+                else
+                    style &= ~WindowStyles.WS_MINIMIZEBOX;
+
+                if (newProperties.IsMaximizable || (newProperties.WindowState == WindowState.Maximized && newProperties.IsResizable))
+                    style |= WindowStyles.WS_MAXIMIZEBOX;
+                else
                     style &= ~WindowStyles.WS_MAXIMIZEBOX;
-                }
 
                 const WindowStyles fullDecorationFlags = WindowStyles.WS_CAPTION | WindowStyles.WS_SYSMENU | WindowStyles.WS_BORDER;
 
@@ -1656,6 +1680,8 @@ namespace Avalonia.Win32
         {
             public bool ShowInTaskbar;
             public bool IsResizable;
+            public bool IsMinimizable;
+            public bool IsMaximizable;
             public SystemDecorations Decorations;
             public bool IsFullScreen;
             public WindowState WindowState;

+ 17 - 0
tests/Avalonia.Controls.UnitTests/WindowTests.cs

@@ -672,6 +672,23 @@ namespace Avalonia.Controls.UnitTests
             }
         }
 
+        [Fact]
+        public void CanMaximize_Should_Be_False_If_CanResize_Is_False()
+        {
+            var windowImpl = MockWindowingPlatform.CreateWindowMock();
+
+            using var app = UnitTestApplication.Start(TestServices.StyledWindow.With(
+                windowingPlatform: new MockWindowingPlatform(() => windowImpl.Object)));
+
+            var window = new Window();
+
+            Assert.True(window.CanMaximize);
+
+            window.CanResize = false;
+
+            Assert.False(window.CanMaximize);
+        }
+
         public class SizingTests : ScopedTestBase
         {
             [Fact]

+ 42 - 1
tests/Avalonia.IntegrationTests.Appium/WindowTests.cs

@@ -311,6 +311,37 @@ namespace Avalonia.IntegrationTests.Appium
             }
         }
 
+        [Theory]
+        [InlineData(false)]
+        [InlineData(true)]
+        public void Window_Minimize_Button_Enabled_Matches_CanMinimize(bool canMinimize)
+        {
+            using (OpenWindow(canMinimize: canMinimize))
+            {
+                var secondaryWindow = Session.GetWindowById("SecondaryWindow");
+                var windowChrome = secondaryWindow.GetSystemChromeButtons();
+
+                Assert.NotNull(windowChrome.Minimize);
+                Assert.Equal(canMinimize, windowChrome.Minimize!.Enabled);
+            }
+        }
+
+        [Theory]
+        [InlineData(false)]
+        [InlineData(true)]
+        public void Window_Maximize_Button_Enabled_Matches_CanMaximize(bool canMaximize)
+        {
+            using (OpenWindow(canMaximize: canMaximize))
+            {
+                var secondaryWindow = Session.GetWindowById("SecondaryWindow");
+                var windowChrome = secondaryWindow.GetSystemChromeButtons();
+
+                var maximizeButton = windowChrome.FullScreen ?? windowChrome.Maximize;
+                Assert.NotNull(maximizeButton);
+                Assert.Equal(canMaximize, maximizeButton.Enabled);
+            }
+        }
+
         [Fact]
         public void Changing_SystemDecorations_Should_Not_Change_Frame_Size_And_Position()
         {
@@ -452,13 +483,17 @@ namespace Avalonia.IntegrationTests.Appium
             WindowStartupLocation location = WindowStartupLocation.Manual,
             WindowState state = Controls.WindowState.Normal,
             bool canResize = true,
-            bool extendClientArea = false)
+            bool extendClientArea = false,
+            bool canMinimize = true,
+            bool canMaximize = true)
         {
             var sizeTextBox = Session.FindElementByAccessibilityId("ShowWindowSize");
             var modeComboBox = Session.FindElementByAccessibilityId("ShowWindowMode");
             var locationComboBox = Session.FindElementByAccessibilityId("ShowWindowLocation");
             var stateComboBox = Session.FindElementByAccessibilityId("ShowWindowState");
             var canResizeCheckBox = Session.FindElementByAccessibilityId("ShowWindowCanResize");
+            var canMinimizeCheckBox = Session.FindElementByAccessibilityId("ShowWindowCanMinimize");
+            var canMaximizeCheckBox = Session.FindElementByAccessibilityId("ShowWindowCanMaximize");
             var showButton = Session.FindElementByAccessibilityId("ShowWindow");
             var extendClientAreaCheckBox = Session.FindElementByAccessibilityId("ShowWindowExtendClientAreaToDecorationsHint");
 
@@ -486,6 +521,12 @@ namespace Avalonia.IntegrationTests.Appium
             if (canResizeCheckBox.GetIsChecked() != canResize)
                 canResizeCheckBox.Click();
 
+            if (canMinimizeCheckBox.GetIsChecked() != canMinimize)
+                canMinimizeCheckBox.Click();
+
+            if (canMaximizeCheckBox.GetIsChecked() != canMaximize)
+                canMaximizeCheckBox.Click();
+
             if (extendClientAreaCheckBox.GetIsChecked() != extendClientArea)
                 extendClientAreaCheckBox.Click();