From 412dca3aaeff60515e003b2842b88070f98719bb Mon Sep 17 00:00:00 2001 From: Nikita Tsukanov Date: Tue, 31 Mar 2026 16:49:29 +0500 Subject: [PATCH] Expose allowed titlebar button actions from the platform implementation and respect them from drawn decorations (#21035) * Expose WM window action capabilities from X11. * Hide titlebar buttons for unsupported actions * Simplify subscription * Explose NetSupported property from X11Globals.cs, so it's easier to make checks from the rest of the codebase * shifts * comma --- .../Chrome/WindowDrawnDecorations.cs | 53 +++++++++++++++---- src/Avalonia.Controls/Platform/IWindowImpl.cs | 10 ++++ .../Platform/PlatformAllowedWindowActions.cs | 33 ++++++++++++ src/Avalonia.Controls/Window.cs | 16 ++++++ .../Controls/WindowDrawnDecorations.xaml | 14 +++++ .../Controls/WindowDrawnDecorations.xaml | 14 +++++ src/Avalonia.X11/X11Atoms.cs | 4 ++ src/Avalonia.X11/X11Globals.cs | 25 +++++++-- src/Avalonia.X11/X11Window.cs | 30 +++++++++++ .../X11WindowModes/DefaultWindowMode.cs | 3 +- 10 files changed, 185 insertions(+), 17 deletions(-) create mode 100644 src/Avalonia.Controls/Platform/PlatformAllowedWindowActions.cs diff --git a/src/Avalonia.Controls/Chrome/WindowDrawnDecorations.cs b/src/Avalonia.Controls/Chrome/WindowDrawnDecorations.cs index 7a282bf306..26dffa8f5d 100644 --- a/src/Avalonia.Controls/Chrome/WindowDrawnDecorations.cs +++ b/src/Avalonia.Controls/Chrome/WindowDrawnDecorations.cs @@ -1,6 +1,7 @@ using System; using Avalonia.Automation; using Avalonia.Controls.Metadata; +using Avalonia.Controls.Platform; using Avalonia.Controls.Primitives; using Avalonia.Layout; using Avalonia.LogicalTree; @@ -15,7 +16,8 @@ namespace Avalonia.Controls.Chrome; /// TopLevelHost extracts overlay/underlay/popover visuals from the template content /// and inserts them into its own visual tree. /// -[PseudoClasses(pcNormal, pcMaximized, pcFullscreen, pcHasShadow, pcHasBorder, pcHasTitlebar)] +[PseudoClasses(pcNormal, pcMaximized, pcFullscreen, pcHasShadow, pcHasBorder, pcHasTitlebar, + pcHasMaximize, pcHasFullscreen, pcHasMinimize)] [TemplatePart(PART_CloseButton, typeof(Button))] [TemplatePart(PART_MinimizeButton, typeof(Button))] [TemplatePart(PART_MaximizeButton, typeof(Button))] @@ -31,6 +33,9 @@ public class WindowDrawnDecorations : StyledElement internal const string pcHasShadow = ":has-shadow"; internal const string pcHasBorder = ":has-border"; internal const string pcHasTitlebar = ":has-titlebar"; + internal const string pcHasMaximize = ":has-maximize"; + internal const string pcHasFullscreen = ":has-fullscreen"; + internal const string pcHasMinimize = ":has-minimize"; // Template part names for caption buttons internal const string PART_CloseButton = "PART_CloseButton"; @@ -383,8 +388,11 @@ public class WindowDrawnDecorations : StyledElement Detach(); _hostWindow = window; + window.AllowedWindowActionsChanged += OnAllowedWindowActionsChanged; + _windowSubscriptions = new CompositeDisposable { + Disposable.Create(() => window.AllowedWindowActionsChanged -= OnAllowedWindowActionsChanged), window.GetObservable(Window.TitleProperty) .Subscribe(title => SetCurrentValue(TitleProperty, title)), window.GetObservable(Window.CanMaximizeProperty) @@ -407,6 +415,7 @@ public class WindowDrawnDecorations : StyledElement }), }; + UpdateAllowedActionsPseudoClasses(); UpdateMaximizeButtonState(); UpdateMinimizeButtonState(); UpdateFullScreenButtonState(); @@ -547,32 +556,54 @@ public class WindowDrawnDecorations : StyledElement e.Handled = true; } + private PlatformAllowedWindowActions EffectiveAllowedActions => + _hostWindow?.AllowedWindowActions ?? PlatformAllowedWindowActions.All; + private void UpdateMaximizeButtonState() { if (_maximizeButton == null) return; - _maximizeButton.IsEnabled = _hostWindow?.WindowState switch - { - WindowState.Maximized or WindowState.FullScreen => _hostWindow.CanResize, - WindowState.Normal => _hostWindow.CanMaximize, - _ => true - }; + _maximizeButton.IsEnabled = EffectiveAllowedActions.HasFlag(PlatformAllowedWindowActions.Maximize) + && (_hostWindow?.WindowState switch + { + WindowState.Maximized or WindowState.FullScreen => _hostWindow.CanResize, + WindowState.Normal => _hostWindow.CanMaximize, + _ => true + }); } private void UpdateMinimizeButtonState() { if (_minimizeButton == null) return; - _minimizeButton.IsEnabled = _hostWindow?.CanMinimize ?? true; + _minimizeButton.IsEnabled = EffectiveAllowedActions.HasFlag(PlatformAllowedWindowActions.Minimize) + && (_hostWindow?.CanMinimize ?? true); } private void UpdateFullScreenButtonState() { if (_fullScreenButton == null) return; - _fullScreenButton.IsEnabled = _hostWindow?.WindowState == WindowState.FullScreen - ? _hostWindow.CanResize - : _hostWindow?.CanMaximize ?? true; + _fullScreenButton.IsEnabled = EffectiveAllowedActions.HasFlag(PlatformAllowedWindowActions.Fullscreen) + && (_hostWindow?.WindowState == WindowState.FullScreen + ? _hostWindow.CanResize + : _hostWindow?.CanMaximize ?? true); + } + + private void OnAllowedWindowActionsChanged(PlatformAllowedWindowActions actions) + { + UpdateAllowedActionsPseudoClasses(); + UpdateMaximizeButtonState(); + UpdateMinimizeButtonState(); + UpdateFullScreenButtonState(); + } + + private void UpdateAllowedActionsPseudoClasses() + { + var actions = EffectiveAllowedActions; + PseudoClasses.Set(pcHasMaximize, actions.HasFlag(PlatformAllowedWindowActions.Maximize)); + PseudoClasses.Set(pcHasFullscreen, actions.HasFlag(PlatformAllowedWindowActions.Fullscreen)); + PseudoClasses.Set(pcHasMinimize, actions.HasFlag(PlatformAllowedWindowActions.Minimize)); } private void UpdateEffectiveGeometry() diff --git a/src/Avalonia.Controls/Platform/IWindowImpl.cs b/src/Avalonia.Controls/Platform/IWindowImpl.cs index 42be775f0d..d864ca35c3 100644 --- a/src/Avalonia.Controls/Platform/IWindowImpl.cs +++ b/src/Avalonia.Controls/Platform/IWindowImpl.cs @@ -162,5 +162,15 @@ namespace Avalonia.Platform /// /// -1 for platform default, otherwise the height in DIPs. void SetExtendClientAreaTitleBarHeightHint(double titleBarHeight); + + /// + /// Gets the window actions that the underlying platform currently allows. + /// + PlatformAllowedWindowActions AllowedWindowActions => PlatformAllowedWindowActions.All; + + /// + /// Gets or sets a callback invoked when changes. + /// + Action? AllowedWindowActionsChanged { get => null; set { } } } } diff --git a/src/Avalonia.Controls/Platform/PlatformAllowedWindowActions.cs b/src/Avalonia.Controls/Platform/PlatformAllowedWindowActions.cs new file mode 100644 index 0000000000..35871c0803 --- /dev/null +++ b/src/Avalonia.Controls/Platform/PlatformAllowedWindowActions.cs @@ -0,0 +1,33 @@ +using System; +using Avalonia.Metadata; + +namespace Avalonia.Controls.Platform; + +/// +/// Flags indicating which window actions the underlying platform supports. +/// +[Flags, PrivateApi] +public enum PlatformAllowedWindowActions +{ + None = 0, + + /// + /// The underlying platform supports maximizing/unmaximizing windows. + /// + Maximize = 1 << 0, + + /// + /// The underlying platform supports fullscreen mode. + /// + Fullscreen = 1 << 1, + + /// + /// The underlying platform supports minimizing windows. + /// + Minimize = 1 << 2, + + /// + /// All actions are supported (default when the underlying platform does not report capabilities). + /// + All = Maximize | Fullscreen | Minimize, +} diff --git a/src/Avalonia.Controls/Window.cs b/src/Avalonia.Controls/Window.cs index 4dab3574eb..0797f39733 100644 --- a/src/Avalonia.Controls/Window.cs +++ b/src/Avalonia.Controls/Window.cs @@ -220,6 +220,7 @@ namespace Avalonia.Controls private bool _positionWasSet; private bool _wasShownBefore; private IDisposable? _modalSubscription; + private PlatformAllowedWindowActions _allowedWindowActions = PlatformAllowedWindowActions.All; /// /// Initializes static members of the class. @@ -250,6 +251,8 @@ namespace Avalonia.Controls impl.WindowStateChanged = HandleWindowStateChanged; _maxPlatformClientSize = PlatformImpl?.MaxAutoSizeHint ?? default(Size); impl.ExtendClientAreaToDecorationsChanged = ExtendClientAreaToDecorationsChanged; + impl.AllowedWindowActionsChanged = OnAllowedWindowActionsChanged; + _allowedWindowActions = impl.AllowedWindowActions; this.GetObservable(ClientSizeProperty).Skip(1).Subscribe(x => { ResizePlatformImpl(x, WindowResizeReason.Application); @@ -485,6 +488,11 @@ namespace Avalonia.Controls set => SetValue(CanMaximizeProperty, value); } + /// + /// Gets the window actions currently allowed by the underlying platform. + /// + internal PlatformAllowedWindowActions AllowedWindowActions => _allowedWindowActions; + /// /// Gets or sets the icon of the window. /// @@ -673,6 +681,14 @@ namespace Avalonia.Controls UpdateDrawnDecorationParts(); } + internal event Action? AllowedWindowActionsChanged; + + private void OnAllowedWindowActionsChanged(PlatformAllowedWindowActions actions) + { + _allowedWindowActions = actions; + AllowedWindowActionsChanged?.Invoke(actions); + } + private void ExtendClientAreaToDecorationsChanged(bool isExtended) { IsExtendedIntoWindowDecorations = isExtended; diff --git a/src/Avalonia.Themes.Fluent/Controls/WindowDrawnDecorations.xaml b/src/Avalonia.Themes.Fluent/Controls/WindowDrawnDecorations.xaml index b2d66fdbbd..3ccda42ce3 100644 --- a/src/Avalonia.Themes.Fluent/Controls/WindowDrawnDecorations.xaml +++ b/src/Avalonia.Themes.Fluent/Controls/WindowDrawnDecorations.xaml @@ -196,6 +196,20 @@ + + + + + + + + + + + +