diff --git a/native/Avalonia.Native/inc/avalonia-native.h b/native/Avalonia.Native/inc/avalonia-native.h index 7782165263..b86c679397 100644 --- a/native/Avalonia.Native/inc/avalonia-native.h +++ b/native/Avalonia.Native/inc/avalonia-native.h @@ -258,6 +258,7 @@ AVNCOM(IAvnWindowBase, 02) : IUnknown virtual HRESULT ObtainNSViewHandleRetained(void** retOut) = 0; virtual HRESULT BeginDragAndDropOperation(AvnDragDropEffects effects, AvnPoint point, IAvnClipboard* clipboard, IAvnDndResultCallback* cb, void* sourceHandle) = 0; + virtual HRESULT SetBlurEnabled (bool enable) = 0; }; AVNCOM(IAvnPopup, 03) : virtual IAvnWindowBase diff --git a/native/Avalonia.Native/src/OSX/window.h b/native/Avalonia.Native/src/OSX/window.h index ca60914526..bdf3007a28 100644 --- a/native/Avalonia.Native/src/OSX/window.h +++ b/native/Avalonia.Native/src/OSX/window.h @@ -3,6 +3,9 @@ class WindowBaseImpl; +@interface AutoFitContentVisualEffectView : NSVisualEffectView +@end + @interface AvnView : NSView -(AvnView* _Nonnull) initWithParent: (WindowBaseImpl* _Nonnull) parent; -(NSEvent* _Nonnull) lastMouseDownEvent; diff --git a/native/Avalonia.Native/src/OSX/window.mm b/native/Avalonia.Native/src/OSX/window.mm index fed2176580..abfae3cf1e 100644 --- a/native/Avalonia.Native/src/OSX/window.mm +++ b/native/Avalonia.Native/src/OSX/window.mm @@ -20,6 +20,7 @@ public: View = NULL; Window = NULL; } + NSVisualEffectView* VisualEffect; AvnView* View; AvnWindow* Window; ComPtr BaseEvents; @@ -47,6 +48,12 @@ public: [Window setStyleMask:NSWindowStyleMaskBorderless]; [Window setBackingType:NSBackingStoreBuffered]; + + VisualEffect = [AutoFitContentVisualEffectView new]; + [VisualEffect setBlendingMode:NSVisualEffectBlendingModeBehindWindow]; + [VisualEffect setMaterial:NSVisualEffectMaterialLight]; + [VisualEffect setAutoresizesSubviews:true]; + [Window setContentView: View]; } @@ -383,6 +390,18 @@ public: return *ppv == nil ? E_FAIL : S_OK; } + virtual HRESULT SetBlurEnabled (bool enable) override + { + [Window setContentView: enable ? VisualEffect : View]; + + if(enable) + { + [VisualEffect addSubview:View]; + } + + return S_OK; + } + virtual HRESULT BeginDragAndDropOperation(AvnDragDropEffects effects, AvnPoint point, IAvnClipboard* clipboard, IAvnDndResultCallback* cb, void* sourceHandle) override @@ -911,6 +930,16 @@ protected: NSArray* AllLoopModes = [NSArray arrayWithObjects: NSDefaultRunLoopMode, NSEventTrackingRunLoopMode, NSModalPanelRunLoopMode, NSRunLoopCommonModes, NSConnectionReplyMode, nil]; +@implementation AutoFitContentVisualEffectView +-(void)setFrameSize:(NSSize)newSize +{ + [super setFrameSize:newSize]; + if([[self subviews] count] == 0) + return; + [[self subviews][0] setFrameSize: newSize]; +} +@end + @implementation AvnView { ComPtr _parent; diff --git a/samples/ControlCatalog/MainView.xaml b/samples/ControlCatalog/MainView.xaml index 30671ef083..add5dbde84 100644 --- a/samples/ControlCatalog/MainView.xaml +++ b/samples/ControlCatalog/MainView.xaml @@ -2,7 +2,7 @@ xmlns:pages="clr-namespace:ControlCatalog.Pages" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" x:Class="ControlCatalog.MainView" - Background="{DynamicResource ThemeBackgroundBrush}" + Background="Transparent" Foreground="{DynamicResource ThemeForegroundBrush}" FontSize="{DynamicResource FontSizeNormal}"> @@ -70,6 +70,12 @@ Light Dark + + None + Transparent + Blur + AcrylicBlur + diff --git a/samples/ControlCatalog/MainView.xaml.cs b/samples/ControlCatalog/MainView.xaml.cs index 7c17b125d6..8c4818fb07 100644 --- a/samples/ControlCatalog/MainView.xaml.cs +++ b/samples/ControlCatalog/MainView.xaml.cs @@ -63,6 +63,13 @@ namespace ControlCatalog if (VisualRoot is Window window) window.SystemDecorations = (SystemDecorations)decorations.SelectedIndex; }; + + var transparencyLevels = this.Find("TransparencyLevels"); + transparencyLevels.SelectionChanged += (sender, e) => + { + if (VisualRoot is Window window) + window.TransparencyLevelHint = (WindowTransparencyLevel)transparencyLevels.SelectedIndex; + }; } protected override void OnAttachedToVisualTree(VisualTreeAttachmentEventArgs e) diff --git a/samples/ControlCatalog/MainWindow.xaml b/samples/ControlCatalog/MainWindow.xaml index 935db20757..a0bb956425 100644 --- a/samples/ControlCatalog/MainWindow.xaml +++ b/samples/ControlCatalog/MainWindow.xaml @@ -7,8 +7,7 @@ xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:vm="clr-namespace:ControlCatalog.ViewModels" xmlns:v="clr-namespace:ControlCatalog.Views" - x:Class="ControlCatalog.MainWindow" WindowState="{Binding WindowState, Mode=TwoWay}"> - + x:Class="ControlCatalog.MainWindow" WindowState="{Binding WindowState, Mode=TwoWay}" Background="Transparent"> diff --git a/src/Avalonia.Controls/Embedding/Offscreen/OffscreenTopLevelImpl.cs b/src/Avalonia.Controls/Embedding/Offscreen/OffscreenTopLevelImpl.cs index 29f0374301..09f9ed7c1b 100644 --- a/src/Avalonia.Controls/Embedding/Offscreen/OffscreenTopLevelImpl.cs +++ b/src/Avalonia.Controls/Embedding/Offscreen/OffscreenTopLevelImpl.cs @@ -49,6 +49,9 @@ namespace Avalonia.Controls.Embedding.Offscreen public Action Paint { get; set; } public Action Resized { get; set; } public Action ScalingChanged { get; set; } + + public Action TransparencyLevelChanged { get; set; } + public void SetInputRoot(IInputRoot inputRoot) => InputRoot = inputRoot; public virtual Point PointToClient(PixelPoint point) => point.ToPoint(1); @@ -61,6 +64,11 @@ namespace Avalonia.Controls.Embedding.Offscreen public Action Closed { get; set; } public abstract IMouseDevice MouseDevice { get; } + + public void SetTransparencyLevelHint(WindowTransparencyLevel transparencyLevel) { } + + public WindowTransparencyLevel TransparencyLevel { get; private set; } + public IPopupImpl CreatePopup() => null; } } diff --git a/src/Avalonia.Controls/Platform/ITopLevelImpl.cs b/src/Avalonia.Controls/Platform/ITopLevelImpl.cs index 98ee17ee1f..c7875f6413 100644 --- a/src/Avalonia.Controls/Platform/ITopLevelImpl.cs +++ b/src/Avalonia.Controls/Platform/ITopLevelImpl.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using Avalonia.Controls; using Avalonia.Input; using Avalonia.Input.Raw; using Avalonia.Rendering; @@ -58,6 +59,11 @@ namespace Avalonia.Platform /// Action ScalingChanged { get; set; } + /// + /// Gets or sets a method called when the toplevel's TransparencyLevel changes. + /// + Action TransparencyLevelChanged { get; set; } + /// /// Creates a new renderer for the toplevel. /// @@ -106,5 +112,15 @@ namespace Avalonia.Platform IMouseDevice MouseDevice { get; } IPopupImpl CreatePopup(); + + /// + /// Sets the hint of the TopLevel. + /// + void SetTransparencyLevelHint(WindowTransparencyLevel transparencyLevel); + + /// + /// Gets the current of the TopLevel. + /// + WindowTransparencyLevel TransparencyLevel { get; } } } diff --git a/src/Avalonia.Controls/TopLevel.cs b/src/Avalonia.Controls/TopLevel.cs index b6eee290ce..8335e03487 100644 --- a/src/Avalonia.Controls/TopLevel.cs +++ b/src/Avalonia.Controls/TopLevel.cs @@ -6,6 +6,7 @@ using Avalonia.Input.Raw; using Avalonia.Layout; using Avalonia.Logging; using Avalonia.LogicalTree; +using Avalonia.Media; using Avalonia.Platform; using Avalonia.Rendering; using Avalonia.Styling; @@ -43,13 +44,35 @@ namespace Avalonia.Controls public static readonly StyledProperty PointerOverElementProperty = AvaloniaProperty.Register(nameof(IInputRoot.PointerOverElement)); + /// + /// Defines the property. + /// + public static readonly StyledProperty TransparencyLevelHintProperty = + AvaloniaProperty.Register(nameof(TransparencyLevelHint), WindowTransparencyLevel.None); + + /// + /// Defines the property. + /// + public static readonly DirectProperty ActualTransparencyLevelProperty = + AvaloniaProperty.RegisterDirect(nameof(ActualTransparencyLevel), + o => o.ActualTransparencyLevel, + unsetValue: WindowTransparencyLevel.None); + + /// + /// Defines the property. + /// + public static readonly StyledProperty TransparencyBackgroundFallbackProperty = + AvaloniaProperty.Register(nameof(TransparencyBackgroundFallback), Brushes.White); + private readonly IInputManager _inputManager; private readonly IAccessKeyHandler _accessKeyHandler; private readonly IKeyboardNavigationHandler _keyboardNavigationHandler; private readonly IPlatformRenderInterface _renderInterface; private readonly IGlobalStyles _globalStyles; private Size _clientSize; + private WindowTransparencyLevel _actualTransparencyLevel; private ILayoutManager _layoutManager; + private Border _transparencyFallbackBorder; /// /// Initializes static members of the class. @@ -57,6 +80,16 @@ namespace Avalonia.Controls static TopLevel() { AffectsMeasure(ClientSizeProperty); + + TransparencyLevelHintProperty.Changed.AddClassHandler( + (tl, e) => + { + if (tl.PlatformImpl != null) + { + tl.PlatformImpl.SetTransparencyLevelHint((WindowTransparencyLevel)e.NewValue); + tl.HandleTransparencyLevelChanged(tl.PlatformImpl.TransparencyLevel); + } + }); } /// @@ -85,6 +118,8 @@ namespace Avalonia.Controls PlatformImpl = impl; + _actualTransparencyLevel = PlatformImpl.TransparencyLevel; + dependencyResolver = dependencyResolver ?? AvaloniaLocator.Current; var styler = TryGetService(dependencyResolver); @@ -108,6 +143,7 @@ namespace Avalonia.Controls impl.Paint = HandlePaint; impl.Resized = HandleResized; impl.ScalingChanged = HandleScalingChanged; + impl.TransparencyLevelChanged = HandleTransparencyLevelChanged; _keyboardNavigationHandler?.SetOwner(this); _accessKeyHandler?.SetOwner(this); @@ -155,6 +191,34 @@ namespace Avalonia.Controls protected set { SetAndRaise(ClientSizeProperty, ref _clientSize, value); } } + /// + /// Gets or sets the that the TopLevel should use when possible. + /// + public WindowTransparencyLevel TransparencyLevelHint + { + get { return GetValue(TransparencyLevelHintProperty); } + set { SetValue(TransparencyLevelHintProperty, value); } + } + + /// + /// Gets the acheived that the platform was able to provide. + /// + public WindowTransparencyLevel ActualTransparencyLevel + { + get => _actualTransparencyLevel; + private set => SetAndRaise(ActualTransparencyLevelProperty, ref _actualTransparencyLevel, value); + } + + /// + /// Gets or sets the that transparency will blend with when transparency is not supported. + /// By default this is a solid white brush. + /// + public IBrush TransparencyBackgroundFallback + { + get => GetValue(TransparencyBackgroundFallbackProperty); + set => SetValue(TransparencyBackgroundFallbackProperty, value); + } + public ILayoutManager LayoutManager { get @@ -312,6 +376,39 @@ namespace Avalonia.Controls LayoutHelper.InvalidateSelfAndChildrenMeasure(this); } + private bool TransparencyLevelsMatch (WindowTransparencyLevel requested, WindowTransparencyLevel received) + { + if(requested == received) + { + return true; + } + else if(requested >= WindowTransparencyLevel.Blur && received >= WindowTransparencyLevel.Blur) + { + return true; + } + + return false; + } + + protected virtual void HandleTransparencyLevelChanged(WindowTransparencyLevel transparencyLevel) + { + if(_transparencyFallbackBorder != null) + { + if(transparencyLevel == WindowTransparencyLevel.None || + TransparencyLevelHint == WindowTransparencyLevel.None || + !TransparencyLevelsMatch(TransparencyLevelHint, transparencyLevel)) + { + _transparencyFallbackBorder.Background = TransparencyBackgroundFallback; + } + else + { + _transparencyFallbackBorder.Background = Brushes.Transparent; + } + } + + ActualTransparencyLevel = transparencyLevel; + } + /// protected override void OnAttachedToVisualTree(VisualTreeAttachmentEventArgs e) { @@ -321,6 +418,15 @@ namespace Avalonia.Controls $"Control '{GetType().Name}' is a top level control and cannot be added as a child."); } + protected override void OnApplyTemplate(TemplateAppliedEventArgs e) + { + base.OnApplyTemplate(e); + + _transparencyFallbackBorder = e.NameScope.Find("PART_TransparencyFallback"); + + HandleTransparencyLevelChanged(PlatformImpl.TransparencyLevel); + } + /// /// Raises the event. /// diff --git a/src/Avalonia.Controls/WindowTransparencyLevel.cs b/src/Avalonia.Controls/WindowTransparencyLevel.cs new file mode 100644 index 0000000000..ce7c03efbb --- /dev/null +++ b/src/Avalonia.Controls/WindowTransparencyLevel.cs @@ -0,0 +1,25 @@ +namespace Avalonia.Controls +{ + public enum WindowTransparencyLevel + { + /// + /// The window background is Black where nothing is drawn in the window. + /// + None, + + /// + /// The window background is Transparent where nothing is drawn in the window. + /// + Transparent, + + /// + /// The window background is a blur-behind where nothing is drawn in the window. + /// + Blur, + + /// + /// The window background is a blur-behind with a high blur radius. This level may fallback to Blur. + /// + AcrylicBlur + } +} diff --git a/src/Avalonia.DesignerSupport/Remote/Stubs.cs b/src/Avalonia.DesignerSupport/Remote/Stubs.cs index 484cf3bc97..a9665b1519 100644 --- a/src/Avalonia.DesignerSupport/Remote/Stubs.cs +++ b/src/Avalonia.DesignerSupport/Remote/Stubs.cs @@ -37,6 +37,8 @@ namespace Avalonia.DesignerSupport.Remote public WindowState WindowState { get; set; } public Action WindowStateChanged { get; set; } + public Action TransparencyLevelChanged { get; set; } + public WindowStub(IWindowImpl parent = null) { if (parent != null) @@ -141,6 +143,10 @@ namespace Avalonia.DesignerSupport.Remote public IPopupPositioner PopupPositioner { get; } public Action GotInputWhenDisabled { get; set; } + + public void SetTransparencyLevelHint(WindowTransparencyLevel transparencyLevel) { } + + public WindowTransparencyLevel TransparencyLevel { get; private set; } } class ClipboardStub : IClipboard diff --git a/src/Avalonia.Native/WindowImplBase.cs b/src/Avalonia.Native/WindowImplBase.cs index 47127fafe0..a7ca528b2b 100644 --- a/src/Avalonia.Native/WindowImplBase.cs +++ b/src/Avalonia.Native/WindowImplBase.cs @@ -369,6 +369,8 @@ namespace Avalonia.Native Action ITopLevelImpl.ScalingChanged { get; set; } + public Action TransparencyLevelChanged { get; set; } + public IScreenImpl Screen { get; private set; } // TODO @@ -389,6 +391,29 @@ namespace Avalonia.Native _native.BeginDragAndDropOperation(effects, point, clipboard, callback, sourceHandle); } + public void SetTransparencyLevelHint(WindowTransparencyLevel transparencyLevel) + { + if (TransparencyLevel != transparencyLevel) + { + if (transparencyLevel >= WindowTransparencyLevel.Blur) + { + transparencyLevel = WindowTransparencyLevel.AcrylicBlur; + } + + if(transparencyLevel == WindowTransparencyLevel.None) + { + transparencyLevel = WindowTransparencyLevel.Transparent; + } + + TransparencyLevel = transparencyLevel; + + _native.SetBlurEnabled(TransparencyLevel >= WindowTransparencyLevel.Blur); + TransparencyLevelChanged?.Invoke(TransparencyLevel); + } + } + + public WindowTransparencyLevel TransparencyLevel { get; private set; } = WindowTransparencyLevel.Transparent; + public IPlatformHandle Handle { get; private set; } } } diff --git a/src/Avalonia.Themes.Default/EmbeddableControlRoot.xaml b/src/Avalonia.Themes.Default/EmbeddableControlRoot.xaml index 1fd168c009..9ffe51fae8 100644 --- a/src/Avalonia.Themes.Default/EmbeddableControlRoot.xaml +++ b/src/Avalonia.Themes.Default/EmbeddableControlRoot.xaml @@ -3,14 +3,17 @@ - - - - - + + + + + + + + diff --git a/src/Avalonia.Themes.Default/OverlayPopupHost.xaml b/src/Avalonia.Themes.Default/OverlayPopupHost.xaml index 56b3699c07..36dbc1b761 100644 --- a/src/Avalonia.Themes.Default/OverlayPopupHost.xaml +++ b/src/Avalonia.Themes.Default/OverlayPopupHost.xaml @@ -2,13 +2,16 @@ - - - + + + + + + diff --git a/src/Avalonia.Themes.Default/PopupRoot.xaml b/src/Avalonia.Themes.Default/PopupRoot.xaml index cf063f5390..9af4f5a910 100644 --- a/src/Avalonia.Themes.Default/PopupRoot.xaml +++ b/src/Avalonia.Themes.Default/PopupRoot.xaml @@ -2,13 +2,16 @@ - - - + + + + + + diff --git a/src/Avalonia.Themes.Default/Window.xaml b/src/Avalonia.Themes.Default/Window.xaml index 2a8b5d0fca..0e7ec42856 100644 --- a/src/Avalonia.Themes.Default/Window.xaml +++ b/src/Avalonia.Themes.Default/Window.xaml @@ -4,16 +4,19 @@ - - - - - + + + + + + + + diff --git a/src/Avalonia.X11/TransparencyHelper.cs b/src/Avalonia.X11/TransparencyHelper.cs new file mode 100644 index 0000000000..0578680136 --- /dev/null +++ b/src/Avalonia.X11/TransparencyHelper.cs @@ -0,0 +1,82 @@ +using System; +using Avalonia.Controls; + +namespace Avalonia.X11 +{ + class TransparencyHelper : IDisposable, X11Globals.IGlobalsSubscriber + { + private readonly X11Info _x11; + private readonly IntPtr _window; + private readonly X11Globals _globals; + private WindowTransparencyLevel _currentLevel; + private WindowTransparencyLevel _requestedLevel; + private bool _isCompositing; + private bool _blurAtomsAreSet; + + public Action TransparencyLevelChanged { get; set; } + public WindowTransparencyLevel CurrentLevel => _currentLevel; + + public TransparencyHelper(X11Info x11, IntPtr window, X11Globals globals) + { + _x11 = x11; + _window = window; + _globals = globals; + _globals.AddSubscriber(this); + } + + public void SetTransparencyRequest(WindowTransparencyLevel level) + { + _requestedLevel = level; + UpdateTransparency(); + } + + private void UpdateTransparency() + { + var newLevel = UpdateAtomsAndGetTransparency(); + if (newLevel != _currentLevel) + { + _currentLevel = newLevel; + TransparencyLevelChanged?.Invoke(newLevel); + } + } + + private WindowTransparencyLevel UpdateAtomsAndGetTransparency() + { + if (_requestedLevel >= WindowTransparencyLevel.Blur) + { + if (!_blurAtomsAreSet) + { + IntPtr value = IntPtr.Zero; + XLib.XChangeProperty(_x11.Display, _window, _x11.Atoms._KDE_NET_WM_BLUR_BEHIND_REGION, + _x11.Atoms.XA_CARDINAL, 32, PropertyMode.Replace, ref value, 1); + _blurAtomsAreSet = true; + } + } + else + { + if (_blurAtomsAreSet) + { + XLib.XDeleteProperty(_x11.Display, _window, _x11.Atoms._KDE_NET_WM_BLUR_BEHIND_REGION); + _blurAtomsAreSet = false; + } + } + + if (!_globals.IsCompositionEnabled) + return WindowTransparencyLevel.None; + if (_requestedLevel >= WindowTransparencyLevel.Blur && CanBlur) + return WindowTransparencyLevel.Blur; + return WindowTransparencyLevel.Transparent; + } + + private bool CanBlur => _globals.WmName == "KWin" && _globals.IsCompositionEnabled; + + public void Dispose() + { + _globals.RemoveSubscriber(this); + } + + void X11Globals.IGlobalsSubscriber.WmChanged(string wmName) => UpdateTransparency(); + + void X11Globals.IGlobalsSubscriber.CompositionChanged(bool compositing) => UpdateTransparency(); + } +} diff --git a/src/Avalonia.X11/X11Atoms.cs b/src/Avalonia.X11/X11Atoms.cs index 523b65c115..dd16fae386 100644 --- a/src/Avalonia.X11/X11Atoms.cs +++ b/src/Avalonia.X11/X11Atoms.cs @@ -185,6 +185,8 @@ namespace Avalonia.X11 public readonly IntPtr UTF8_STRING; public readonly IntPtr UTF16_STRING; public readonly IntPtr ATOM_PAIR; + public readonly IntPtr MANAGER; + public readonly IntPtr _KDE_NET_WM_BLUR_BEHIND_REGION; public X11Atoms(IntPtr display) diff --git a/src/Avalonia.X11/X11Globals.cs b/src/Avalonia.X11/X11Globals.cs new file mode 100644 index 0000000000..afde15b808 --- /dev/null +++ b/src/Avalonia.X11/X11Globals.cs @@ -0,0 +1,182 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Runtime.InteropServices; +using static Avalonia.X11.XLib; +namespace Avalonia.X11 +{ + unsafe class X11Globals + { + private readonly AvaloniaX11Platform _plat; + private readonly int _screenNumber; + private readonly X11Info _x11; + private readonly IntPtr _rootWindow; + private readonly IntPtr _compositingAtom; + private readonly List _subscribers = new List(); + + private string _wmName; + private IntPtr _compositionAtomOwner; + private bool _isCompositionEnabled; + + public X11Globals(AvaloniaX11Platform plat) + { + _plat = plat; + _x11 = plat.Info; + _screenNumber = XDefaultScreen(_x11.Display); + _rootWindow = XRootWindow(_x11.Display, _screenNumber); + plat.Windows[_rootWindow] = OnRootWindowEvent; + XSelectInput(_x11.Display, _rootWindow, + new IntPtr((int)(EventMask.StructureNotifyMask | EventMask.PropertyChangeMask))); + _compositingAtom = XInternAtom(_x11.Display, "_NET_WM_CM_S" + _screenNumber, false); + UpdateWmName(); + UpdateCompositingAtomOwner(); + } + + public string WmName + { + get => _wmName; + private set + { + if (_wmName != value) + { + _wmName = value; + // The collection might change during enumeration + foreach (var s in _subscribers.ToList()) + s.WmChanged(value); + } + } + } + + private IntPtr CompositionAtomOwner + { + get => _compositionAtomOwner; + set + { + if (_compositionAtomOwner != value) + { + _compositionAtomOwner = value; + IsCompositionEnabled = _compositionAtomOwner != IntPtr.Zero; + } + } + } + + public bool IsCompositionEnabled + { + get => _isCompositionEnabled; + set + { + if (_isCompositionEnabled != value) + { + _isCompositionEnabled = value; + // The collection might change during enumeration + foreach (var s in _subscribers.ToList()) + s.CompositionChanged(value); + } + } + } + + IntPtr GetSupportingWmCheck(IntPtr window) + { + XGetWindowProperty(_x11.Display, _rootWindow, _x11.Atoms._NET_SUPPORTING_WM_CHECK, + IntPtr.Zero, new IntPtr(IntPtr.Size), false, + _x11.Atoms.XA_WINDOW, out IntPtr actualType, out int actualFormat, out IntPtr nitems, + out IntPtr bytesAfter, out IntPtr prop); + if (nitems.ToInt32() != 1) + return IntPtr.Zero; + try + { + if (actualType != _x11.Atoms.XA_WINDOW) + return IntPtr.Zero; + return *(IntPtr*)prop.ToPointer(); + } + finally + { + XFree(prop); + } + } + + void UpdateCompositingAtomOwner() + { + // This procedure is described in https://tronche.com/gui/x/icccm/sec-2.html#s-2.8 + + // Check the server-side selection owner + var newOwner = XGetSelectionOwner(_x11.Display, _compositingAtom); + while (CompositionAtomOwner != newOwner) + { + // We have a new owner, unsubscribe from the previous one first + if (CompositionAtomOwner != IntPtr.Zero) + { + _plat.Windows.Remove(CompositionAtomOwner); + XSelectInput(_x11.Display, CompositionAtomOwner, IntPtr.Zero); + } + + // Set it as the current owner and select input + CompositionAtomOwner = newOwner; + if (CompositionAtomOwner != IntPtr.Zero) + { + _plat.Windows[newOwner] = HandleCompositionAtomOwnerEvents; + XSelectInput(_x11.Display, CompositionAtomOwner, new IntPtr((int)(EventMask.StructureNotifyMask))); + } + + // Check for the new owner again and repeat the procedure if it was changed between XGetSelectionOwner and XSelectInput call + newOwner = XGetSelectionOwner(_x11.Display, _compositingAtom); + } + } + + private void HandleCompositionAtomOwnerEvents(XEvent ev) + { + if(ev.type == XEventName.DestroyNotify) + UpdateCompositingAtomOwner(); + } + + void UpdateWmName() => WmName = GetWmName(); + + string GetWmName() + { + var wm = GetSupportingWmCheck(_rootWindow); + if (wm == IntPtr.Zero || wm != GetSupportingWmCheck(wm)) + return null; + XGetWindowProperty(_x11.Display, wm, _x11.Atoms._NET_WM_NAME, + IntPtr.Zero, new IntPtr(0x7fffffff), + false, _x11.Atoms.UTF8_STRING, out var actualType, out var actualFormat, + out var nitems, out _, out var prop); + if (nitems == IntPtr.Zero) + return null; + try + { + if (actualFormat != 8) + return null; + return Marshal.PtrToStringAnsi(prop, nitems.ToInt32()); + } + finally + { + XFree(prop); + } + } + + private void OnRootWindowEvent(XEvent ev) + { + if (ev.type == XEventName.PropertyNotify) + { + if(ev.PropertyEvent.atom == _x11.Atoms._NET_SUPPORTING_WM_CHECK) + UpdateWmName(); + } + + if (ev.type == XEventName.ClientMessage) + { + if(ev.ClientMessageEvent.message_type == _x11.Atoms.MANAGER + && ev.ClientMessageEvent.ptr2 == _compositingAtom) + UpdateCompositingAtomOwner(); + } + } + + public interface IGlobalsSubscriber + { + void WmChanged(string wmName); + void CompositionChanged(bool compositing); + } + + public void AddSubscriber(IGlobalsSubscriber subscriber) => _subscribers.Add(subscriber); + public void RemoveSubscriber(IGlobalsSubscriber subscriber) => _subscribers.Remove(subscriber); + } +} diff --git a/src/Avalonia.X11/X11Platform.cs b/src/Avalonia.X11/X11Platform.cs index a1bfa682f5..c2b3926ffd 100644 --- a/src/Avalonia.X11/X11Platform.cs +++ b/src/Avalonia.X11/X11Platform.cs @@ -26,6 +26,7 @@ namespace Avalonia.X11 public IX11Screens X11Screens { get; private set; } public IScreenImpl Screens { get; private set; } public X11PlatformOptions Options { get; private set; } + public X11Globals Globals { get; private set; } public void Initialize(X11PlatformOptions options) { Options = options; @@ -36,6 +37,7 @@ namespace Avalonia.X11 throw new Exception("XOpenDisplay failed"); XError.Init(); Info = new X11Info(Display, DeferredDisplay); + Globals = new X11Globals(this); //TODO: log if (options.UseDBusMenu) DBusHelper.TryInitialize(); diff --git a/src/Avalonia.X11/X11Window.cs b/src/Avalonia.X11/X11Window.cs index 643b037b3f..4899f0efc0 100644 --- a/src/Avalonia.X11/X11Window.cs +++ b/src/Avalonia.X11/X11Window.cs @@ -43,6 +43,7 @@ namespace Avalonia.X11 private bool _wasMappedAtLeastOnce = false; private double? _scalingOverride; private bool _disabled; + private TransparencyHelper _transparencyHelper; public object SyncRoot { get; } = new object(); @@ -176,6 +177,9 @@ namespace Avalonia.X11 UpdateSizeHints(null); _xic = XCreateIC(_x11.Xim, XNames.XNInputStyle, XIMProperties.XIMPreeditNothing | XIMProperties.XIMStatusNothing, XNames.XNClientWindow, _handle, IntPtr.Zero); + _transparencyHelper = new TransparencyHelper(_x11, _handle, platform.Globals); + _transparencyHelper.SetTransparencyRequest(WindowTransparencyLevel.None); + XFlush(_x11.Display); if(_popup) PopupPositioner = new ManagedPopupPositioner(new ManagedPopupPositionerPopupImplHelper(popupParent, MoveResize)); @@ -300,6 +304,13 @@ namespace Avalonia.X11 public Action Activated { get; set; } public Func Closing { get; set; } public Action WindowStateChanged { get; set; } + + public Action TransparencyLevelChanged + { + get => _transparencyHelper.TransparencyLevelChanged; + set => _transparencyHelper.TransparencyLevelChanged = value; + } + public Action Closed { get; set; } public Action PositionChanged { get; set; } @@ -1083,5 +1094,10 @@ namespace Avalonia.X11 public IPopupPositioner PopupPositioner { get; } public ITopLevelNativeMenuExporter NativeMenuExporter { get; } + + public void SetTransparencyLevelHint(WindowTransparencyLevel transparencyLevel) => + _transparencyHelper.SetTransparencyRequest(transparencyLevel); + + public WindowTransparencyLevel TransparencyLevel => _transparencyHelper.CurrentLevel; } } diff --git a/src/Linux/Avalonia.LinuxFramebuffer/FramebufferToplevelImpl.cs b/src/Linux/Avalonia.LinuxFramebuffer/FramebufferToplevelImpl.cs index 9844cdc03b..d4a29e39c6 100644 --- a/src/Linux/Avalonia.LinuxFramebuffer/FramebufferToplevelImpl.cs +++ b/src/Linux/Avalonia.LinuxFramebuffer/FramebufferToplevelImpl.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using Avalonia.Controls; using Avalonia.Input; using Avalonia.Input.Raw; using Avalonia.LinuxFramebuffer.Input; @@ -70,6 +71,9 @@ namespace Avalonia.LinuxFramebuffer public Action Paint { get; set; } public Action Resized { get; set; } public Action ScalingChanged { get; set; } + + public Action TransparencyLevelChanged { get; set; } + public Action Closed { get; set; } public event Action LostFocus { @@ -78,5 +82,9 @@ namespace Avalonia.LinuxFramebuffer } public Size ScaledSize => _outputBackend.PixelSize.ToSize(Scaling); + + public void SetTransparencyLevelHint(WindowTransparencyLevel transparencyLevel) { } + + public WindowTransparencyLevel TransparencyLevel { get; private set; } } } diff --git a/src/Windows/Avalonia.Win32.Interop/Wpf/WpfTopLevelImpl.cs b/src/Windows/Avalonia.Win32.Interop/Wpf/WpfTopLevelImpl.cs index 2887468046..b30831667b 100644 --- a/src/Windows/Avalonia.Win32.Interop/Wpf/WpfTopLevelImpl.cs +++ b/src/Windows/Avalonia.Win32.Interop/Wpf/WpfTopLevelImpl.cs @@ -5,6 +5,7 @@ using System.Windows; using System.Windows.Input; using System.Windows.Interop; using System.Windows.Media; +using Avalonia.Controls; using Avalonia.Controls.Embedding; using Avalonia.Input; using Avalonia.Input.Raw; @@ -236,6 +237,9 @@ namespace Avalonia.Win32.Interop.Wpf Action ITopLevelImpl.Paint { get; set; } Action ITopLevelImpl.Resized { get; set; } Action ITopLevelImpl.ScalingChanged { get; set; } + + Action ITopLevelImpl.TransparencyLevelChanged { get; set; } + Action ITopLevelImpl.Closed { get; set; } public new event Action LostFocus; @@ -248,5 +252,9 @@ namespace Avalonia.Win32.Interop.Wpf } public IPopupImpl CreatePopup() => null; + + public void SetTransparencyLevelHint(WindowTransparencyLevel transparencyLevel) { } + + public WindowTransparencyLevel TransparencyLevel { get; private set; } } } diff --git a/src/Windows/Avalonia.Win32/Interop/UnmanagedMethods.cs b/src/Windows/Avalonia.Win32/Interop/UnmanagedMethods.cs index 5601ccbafe..da52c3116e 100644 --- a/src/Windows/Avalonia.Win32/Interop/UnmanagedMethods.cs +++ b/src/Windows/Avalonia.Win32/Interop/UnmanagedMethods.cs @@ -1302,6 +1302,112 @@ namespace Avalonia.Win32.Interop [DllImport("dwmapi.dll")] public static extern int DwmExtendFrameIntoClientArea(IntPtr hwnd, ref MARGINS margins); + [DllImport("dwmapi.dll")] + public static extern int DwmIsCompositionEnabled(out bool enabled); + + [DllImport("dwmapi.dll")] + public static extern void DwmEnableBlurBehindWindow(IntPtr hwnd, ref DWM_BLURBEHIND blurBehind); + + [Flags] + public enum DWM_BB + { + Enable = 1, + BlurRegion = 2, + TransitionMaximized = 4 + } + + [StructLayout(LayoutKind.Sequential)] + public struct DWM_BLURBEHIND + { + public DWM_BB dwFlags; + public bool fEnable; + public IntPtr hRgnBlur; + public bool fTransitionOnMaximized; + + public DWM_BLURBEHIND(bool enabled) + { + fEnable = enabled ? true : false; + hRgnBlur = IntPtr.Zero; + fTransitionOnMaximized = false; + dwFlags = DWM_BB.Enable; + } + } + + [StructLayout(LayoutKind.Sequential)] + internal struct RTL_OSVERSIONINFOEX + { + internal uint dwOSVersionInfoSize; + internal uint dwMajorVersion; + internal uint dwMinorVersion; + internal uint dwBuildNumber; + internal uint dwPlatformId; + [MarshalAs(UnmanagedType.ByValTStr, SizeConst = 128)] + internal string szCSDVersion; + } + + [DllImport("ntdll")] + private static extern int RtlGetVersion(out RTL_OSVERSIONINFOEX lpVersionInformation); + + internal static Version RtlGetVersion() + { + RTL_OSVERSIONINFOEX v = new RTL_OSVERSIONINFOEX(); + v.dwOSVersionInfoSize = (uint)Marshal.SizeOf(v); + if (RtlGetVersion(out v) == 0) + { + return new Version((int)v.dwMajorVersion, (int)v.dwMinorVersion, (int)v.dwBuildNumber, (int)v.dwPlatformId); + } + else + { + throw new Exception("RtlGetVersion failed!"); + } + } + + [DllImport("user32.dll")] + internal static extern int SetWindowCompositionAttribute(IntPtr hwnd, ref WindowCompositionAttributeData data); + + [StructLayout(LayoutKind.Sequential)] + internal struct WindowCompositionAttributeData + { + public WindowCompositionAttribute Attribute; + public IntPtr Data; + public int SizeOfData; + } + + internal enum WindowCompositionAttribute + { + // ... + WCA_ACCENT_POLICY = 19 + // ... + } + + internal enum AccentState + { + ACCENT_DISABLED = 0, + ACCENT_ENABLE_GRADIENT = 1, + ACCENT_ENABLE_TRANSPARENTGRADIENT = 2, + ACCENT_ENABLE_BLURBEHIND = 3, + ACCENT_ENABLE_ACRYLIC = 4, //1703 and above + ACCENT_ENABLE_HOSTBACKDROP = 5, // RS5 1809 + ACCENT_INVALID_STATE = 6 + } + + internal enum AccentFlags + { + DrawLeftBorder = 0x20, + DrawTopBorder = 0x40, + DrawRightBorder = 0x80, + DrawBottomBorder = 0x100, + } + + [StructLayout(LayoutKind.Sequential)] + internal struct AccentPolicy + { + public AccentState AccentState; + public int AccentFlags; + public int GradientColor; + public int AnimationId; + } + [StructLayout(LayoutKind.Sequential)] internal struct MARGINS { diff --git a/src/Windows/Avalonia.Win32/Win32Platform.cs b/src/Windows/Avalonia.Win32/Win32Platform.cs index c01bc8e16d..e25c5e4733 100644 --- a/src/Windows/Avalonia.Win32/Win32Platform.cs +++ b/src/Windows/Avalonia.Win32/Win32Platform.cs @@ -58,6 +58,11 @@ namespace Avalonia.Win32 CreateMessageWindow(); } + /// + /// Gets the actual WindowsVersion. Same as the info returned from RtlGetVersion. + /// + public static Version WindowsVersion { get; } = RtlGetVersion(); + public static bool UseDeferredRendering => Options.UseDeferredRendering; internal static bool UseOverlayPopups => Options.OverlayPopups; public static Win32PlatformOptions Options { get; private set; } diff --git a/src/Windows/Avalonia.Win32/WindowImpl.cs b/src/Windows/Avalonia.Win32/WindowImpl.cs index cc4c12ec3c..0cf5a73b9f 100644 --- a/src/Windows/Avalonia.Win32/WindowImpl.cs +++ b/src/Windows/Avalonia.Win32/WindowImpl.cs @@ -5,6 +5,7 @@ using System.Runtime.InteropServices; using Avalonia.Controls; using Avalonia.Input; using Avalonia.Input.Raw; +using Avalonia.Media; using Avalonia.OpenGL; using Avalonia.Platform; using Avalonia.Rendering; @@ -122,6 +123,8 @@ namespace Avalonia.Win32 public Action WindowStateChanged { get; set; } + public Action TransparencyLevelChanged { get; set; } + public Thickness BorderThickness { get @@ -206,6 +209,81 @@ namespace Avalonia.Win32 } } + public WindowTransparencyLevel TransparencyLevel { get; private set; } + + public void SetTransparencyLevelHint (WindowTransparencyLevel transparencyLevel) + { + TransparencyLevel = EnableBlur(transparencyLevel); + } + + private WindowTransparencyLevel EnableBlur(WindowTransparencyLevel transparencyLevel) + { + bool canUseTransparency = false; + bool canUseAcrylic = false; + + if (Win32Platform.WindowsVersion.Major >= 10) + { + canUseTransparency = true; + + if (Win32Platform.WindowsVersion.Major > 10 || Win32Platform.WindowsVersion.Build >= 19628) + { + canUseAcrylic = true; + } + } + + if (!canUseTransparency || DwmIsCompositionEnabled(out var compositionEnabled) != 0 || !compositionEnabled) + { + return WindowTransparencyLevel.None; + } + + var accent = new AccentPolicy(); + var accentStructSize = Marshal.SizeOf(accent); + + if(transparencyLevel == WindowTransparencyLevel.AcrylicBlur && !canUseAcrylic) + { + transparencyLevel = WindowTransparencyLevel.Blur; + } + + switch (transparencyLevel) + { + default: + case WindowTransparencyLevel.None: + accent.AccentState = AccentState.ACCENT_DISABLED; + break; + + case WindowTransparencyLevel.Transparent: + accent.AccentState = AccentState.ACCENT_ENABLE_TRANSPARENTGRADIENT; + break; + + case WindowTransparencyLevel.Blur: + accent.AccentState = AccentState.ACCENT_ENABLE_BLURBEHIND; + break; + + case WindowTransparencyLevel.AcrylicBlur: + case (WindowTransparencyLevel.AcrylicBlur + 1): // hack-force acrylic. + accent.AccentState = AccentState.ACCENT_ENABLE_ACRYLIC; + transparencyLevel = WindowTransparencyLevel.AcrylicBlur; + break; + } + + accent.AccentFlags = 2; + accent.GradientColor = 0x00FFFFFF; + + var accentPtr = Marshal.AllocHGlobal(accentStructSize); + Marshal.StructureToPtr(accent, accentPtr, false); + + var data = new WindowCompositionAttributeData(); + data.Attribute = WindowCompositionAttribute.WCA_ACCENT_POLICY; + data.SizeOfData = accentStructSize; + data.Data = accentPtr; + + SetWindowCompositionAttribute(_hwnd, ref data); + + Marshal.FreeHGlobal(accentPtr); + + return transparencyLevel; + } + public IEnumerable Surfaces => new object[] { Handle, _gl, _framebuffer }; public PixelPoint Position @@ -780,9 +858,14 @@ namespace Avalonia.Win32 if (!_isFullScreenActive) { + var margin = newProperties.Decorations == SystemDecorations.BorderOnly ? 1 : 0; + var margins = new MARGINS { - cyBottomHeight = newProperties.Decorations == SystemDecorations.BorderOnly ? 1 : 0 + cyBottomHeight = margin, + cxRightWidth = margin, + cxLeftWidth = margin, + cyTopHeight = margin }; DwmExtendFrameIntoClientArea(_hwnd, ref margins); diff --git a/tests/Avalonia.Controls.UnitTests/Primitives/PopupRootTests.cs b/tests/Avalonia.Controls.UnitTests/Primitives/PopupRootTests.cs index ddf109f80e..08b9c75dbc 100644 --- a/tests/Avalonia.Controls.UnitTests/Primitives/PopupRootTests.cs +++ b/tests/Avalonia.Controls.UnitTests/Primitives/PopupRootTests.cs @@ -77,8 +77,13 @@ namespace Avalonia.Controls.UnitTests.Primitives var templatedChild = ((Visual)target.Host).GetVisualChildren().Single(); - Assert.IsType(templatedChild); - var contentPresenter = templatedChild.VisualChildren.Single(); + Assert.IsType(templatedChild); + + var visualLayerManager = templatedChild.GetVisualChildren().Skip(1).Single(); + + Assert.IsType(visualLayerManager); + + var contentPresenter = visualLayerManager.VisualChildren.Single(); Assert.IsType(contentPresenter); diff --git a/tests/Avalonia.Controls.UnitTests/Primitives/PopupTests.cs b/tests/Avalonia.Controls.UnitTests/Primitives/PopupTests.cs index 0b9c94f850..fd06ba295b 100644 --- a/tests/Avalonia.Controls.UnitTests/Primitives/PopupTests.cs +++ b/tests/Avalonia.Controls.UnitTests/Primitives/PopupTests.cs @@ -275,6 +275,8 @@ namespace Avalonia.Controls.UnitTests.Primitives Assert.Equal( new[] { + "Panel", + "Border", "VisualLayerManager", "ContentPresenter", "ContentPresenter", @@ -289,6 +291,8 @@ namespace Avalonia.Controls.UnitTests.Primitives Assert.Equal( new object[] { + popupRoot, + popupRoot, popupRoot, popupRoot, target,