diff --git a/native/Avalonia.Native/inc/avalonia-native.h b/native/Avalonia.Native/inc/avalonia-native.h index 6800ff7d68..9ff6130e5f 100644 --- a/native/Avalonia.Native/inc/avalonia-native.h +++ b/native/Avalonia.Native/inc/avalonia-native.h @@ -205,6 +205,15 @@ enum AvnMenuItemToggleType Radio }; +enum AvnExtendClientAreaChromeHints +{ + AvnNoChrome = 0, + AvnSystemChrome = 0x01, + AvnPreferSystemChrome = 0x02, + AvnOSXThickTitleBar = 0x08, + AvnDefaultChrome = AvnSystemChrome, +}; + AVNCOM(IAvaloniaNativeFactory, 01) : IUnknown { public: @@ -279,6 +288,10 @@ AVNCOM(IAvnWindow, 04) : virtual IAvnWindowBase virtual HRESULT SetWindowState(AvnWindowState state) = 0; virtual HRESULT GetWindowState(AvnWindowState*ret) = 0; virtual HRESULT TakeFocusFromChildren() = 0; + virtual HRESULT SetExtendClientArea (bool enable) = 0; + virtual HRESULT SetExtendClientAreaHints (AvnExtendClientAreaChromeHints hints) = 0; + virtual HRESULT GetExtendTitleBarHeight (double*ret) = 0; + virtual HRESULT SetExtendTitleBarHeight (double value) = 0; }; AVNCOM(IAvnWindowBaseEvents, 05) : IUnknown diff --git a/native/Avalonia.Native/src/OSX/window.h b/native/Avalonia.Native/src/OSX/window.h index bdf3007a28..b1f64bca88 100644 --- a/native/Avalonia.Native/src/OSX/window.h +++ b/native/Avalonia.Native/src/OSX/window.h @@ -3,9 +3,6 @@ class WindowBaseImpl; -@interface AutoFitContentVisualEffectView : NSVisualEffectView -@end - @interface AvnView : NSView -(AvnView* _Nonnull) initWithParent: (WindowBaseImpl* _Nonnull) parent; -(NSEvent* _Nonnull) lastMouseDownEvent; @@ -15,6 +12,14 @@ class WindowBaseImpl; -(AvnPixelSize) getPixelSize; @end +@interface AutoFitContentView : NSView +-(AutoFitContentView* _Nonnull) initWithContent: (NSView* _Nonnull) content; +-(void) ShowTitleBar: (bool) show; +-(void) SetTitleBarHeightHint: (double) height; +-(void) SetContent: (NSView* _Nonnull) content; +-(void) ShowBlur: (bool) show; +@end + @interface AvnWindow : NSWindow +(void) closeAll; -(AvnWindow* _Nonnull) initWithParent: (WindowBaseImpl* _Nonnull) parent; @@ -27,6 +32,8 @@ class WindowBaseImpl; -(void) showWindowMenuWithAppMenu; -(void) applyMenu:(NSMenu* _Nullable)menu; -(double) getScaling; +-(double) getExtendedTitleBarHeight; +-(void) setIsExtended:(bool)value; @end struct INSWindowHolder diff --git a/native/Avalonia.Native/src/OSX/window.mm b/native/Avalonia.Native/src/OSX/window.mm index 7f8a6e1393..872269bb26 100644 --- a/native/Avalonia.Native/src/OSX/window.mm +++ b/native/Avalonia.Native/src/OSX/window.mm @@ -6,8 +6,6 @@ #include #include "rendertarget.h" - - class WindowBaseImpl : public virtual ComSingleObject, public INSWindowHolder { private: @@ -20,7 +18,7 @@ public: View = NULL; Window = NULL; } - NSVisualEffectView* VisualEffect; + AutoFitContentView* StandardContainer; AvnView* View; AvnWindow* Window; ComPtr BaseEvents; @@ -39,6 +37,7 @@ public: _glContext = gl; renderTarget = [[IOSurfaceRenderTarget alloc] initWithOpenGlContext: gl]; View = [[AvnView alloc] initWithParent:this]; + StandardContainer = [[AutoFitContentView new] initWithContent:View]; Window = [[AvnWindow alloc] initWithParent:this]; @@ -49,12 +48,8 @@ public: [Window setStyleMask:NSWindowStyleMaskBorderless]; [Window setBackingType:NSBackingStoreBuffered]; - VisualEffect = [AutoFitContentVisualEffectView new]; - [VisualEffect setBlendingMode:NSVisualEffectBlendingModeBehindWindow]; - [VisualEffect setMaterial:NSVisualEffectMaterialLight]; - [VisualEffect setAutoresizesSubviews:true]; - - [Window setContentView: View]; + [Window setOpaque:false]; + [Window setContentView: StandardContainer]; } virtual HRESULT ObtainNSWindowHandle(void** ret) override @@ -410,12 +405,7 @@ public: virtual HRESULT SetBlurEnabled (bool enable) override { - [Window setContentView: enable ? VisualEffect : View]; - - if(enable) - { - [VisualEffect addSubview:View]; - } + [StandardContainer ShowBlur:enable]; return S_OK; } @@ -492,6 +482,8 @@ private: bool _inSetWindowState; NSRect _preZoomSize; bool _transitioningWindowState; + bool _isClientAreaExtended; + AvnExtendClientAreaChromeHints _extendClientHints; FORWARD_IUNKNOWN() BEGIN_INTERFACE_MAP() @@ -505,6 +497,8 @@ private: ComPtr WindowEvents; WindowImpl(IAvnWindowEvents* events, IAvnGlContext* gl) : WindowBaseImpl(events, gl) { + _isClientAreaExtended = false; + _extendClientHints = AvnDefaultChrome; _fullScreenActive = false; _canResize = true; _decorations = SystemDecorationsFull; @@ -523,8 +517,20 @@ private: if ([subview isKindOfClass:NSClassFromString(@"NSTitlebarContainerView")]) { NSView *titlebarView = [subview subviews][0]; for (id button in titlebarView.subviews) { - if ([button isKindOfClass:[NSButton class]]) { - [button setHidden: (_decorations != SystemDecorationsFull)]; + if ([button isKindOfClass:[NSButton class]]) + { + if(_isClientAreaExtended) + { + auto wantsChrome = (_extendClientHints & AvnSystemChrome) || (_extendClientHints & AvnPreferSystemChrome); + + [button setHidden: !wantsChrome]; + } + else + { + [button setHidden: (_decorations != SystemDecorationsFull)]; + } + + [button setWantsLayer:true]; } } } @@ -600,6 +606,35 @@ private: if(_lastWindowState != state) { + if(_isClientAreaExtended) + { + if(_lastWindowState == FullScreen) + { + // we exited fs. + if(_extendClientHints & AvnOSXThickTitleBar) + { + Window.toolbar = [NSToolbar new]; + Window.toolbar.showsBaselineSeparator = false; + } + + [Window setTitlebarAppearsTransparent:true]; + + [StandardContainer setFrameSize: StandardContainer.frame.size]; + } + else if(state == FullScreen) + { + // we entered fs. + if(_extendClientHints & AvnOSXThickTitleBar) + { + Window.toolbar = nullptr; + } + + [Window setTitlebarAppearsTransparent:false]; + + [StandardContainer setFrameSize: StandardContainer.frame.size]; + } + } + _lastWindowState = state; WindowEvents->WindowStateChanged(state); } @@ -656,8 +691,6 @@ private: return S_OK; } - auto currentFrame = [Window frame]; - UpdateStyle(); HideOrShowTrafficLights(); @@ -790,6 +823,81 @@ private: return S_OK; if([Window isKeyWindow]) [Window makeFirstResponder: View]; + + return S_OK; + } + + virtual HRESULT SetExtendClientArea (bool enable) override + { + _isClientAreaExtended = enable; + + if(enable) + { + Window.titleVisibility = NSWindowTitleHidden; + + [Window setTitlebarAppearsTransparent:true]; + + auto wantsTitleBar = (_extendClientHints & AvnSystemChrome) || (_extendClientHints & AvnPreferSystemChrome); + + if (wantsTitleBar) + { + [StandardContainer ShowTitleBar:true]; + } + else + { + [StandardContainer ShowTitleBar:false]; + } + + if(_extendClientHints & AvnOSXThickTitleBar) + { + Window.toolbar = [NSToolbar new]; + Window.toolbar.showsBaselineSeparator = false; + } + else + { + Window.toolbar = nullptr; + } + } + else + { + Window.titleVisibility = NSWindowTitleVisible; + Window.toolbar = nullptr; + [Window setTitlebarAppearsTransparent:false]; + View.layer.zPosition = 0; + } + + [Window setIsExtended:enable]; + + HideOrShowTrafficLights(); + + UpdateStyle(); + + return S_OK; + } + + virtual HRESULT SetExtendClientAreaHints (AvnExtendClientAreaChromeHints hints) override + { + _extendClientHints = hints; + + SetExtendClientArea(_isClientAreaExtended); + return S_OK; + } + + virtual HRESULT GetExtendTitleBarHeight (double*ret) override + { + if(ret == nullptr) + { + return E_POINTER; + } + + *ret = [Window getExtendedTitleBarHeight]; + + return S_OK; + } + + virtual HRESULT SetExtendTitleBarHeight (double value) override + { + [StandardContainer SetTitleBarHeightHint:value]; return S_OK; } @@ -802,8 +910,9 @@ private: [Window setTitlebarAppearsTransparent:NO]; [Window setTitle:_lastTitle]; - [Window setStyleMask:NSWindowStyleMaskTitled | NSWindowStyleMaskResizable]; - + Window.styleMask = Window.styleMask | NSWindowStyleMaskTitled | NSWindowStyleMaskResizable; + Window.styleMask = Window.styleMask & ~NSWindowStyleMaskFullSizeContentView; + [Window toggleFullScreen:nullptr]; } @@ -951,19 +1060,120 @@ protected: { s |= NSWindowStyleMaskMiniaturizable; } + + if(_isClientAreaExtended) + { + s |= NSWindowStyleMaskFullSizeContentView | NSWindowStyleMaskTexturedBackground; + } return s; } }; NSArray* AllLoopModes = [NSArray arrayWithObjects: NSDefaultRunLoopMode, NSEventTrackingRunLoopMode, NSModalPanelRunLoopMode, NSRunLoopCommonModes, NSConnectionReplyMode, nil]; -@implementation AutoFitContentVisualEffectView +@implementation AutoFitContentView +{ + NSVisualEffectView* _titleBarMaterial; + NSBox* _titleBarUnderline; + NSView* _content; + NSVisualEffectView* _blurBehind; + double _titleBarHeightHint; + bool _settingSize; +} + +-(AutoFitContentView* _Nonnull) initWithContent:(NSView *)content +{ + _titleBarHeightHint = -1; + _content = content; + _settingSize = false; + + [self setAutoresizesSubviews:true]; + [self setWantsLayer:true]; + + _titleBarMaterial = [NSVisualEffectView new]; + [_titleBarMaterial setBlendingMode:NSVisualEffectBlendingModeWithinWindow]; + [_titleBarMaterial setMaterial:NSVisualEffectMaterialTitlebar]; + [_titleBarMaterial setWantsLayer:true]; + _titleBarMaterial.hidden = true; + + _titleBarUnderline = [NSBox new]; + _titleBarUnderline.boxType = NSBoxSeparator; + _titleBarUnderline.fillColor = [NSColor underPageBackgroundColor]; + _titleBarUnderline.hidden = true; + + [self addSubview:_titleBarMaterial]; + [self addSubview:_titleBarUnderline]; + + _blurBehind = [NSVisualEffectView new]; + [_blurBehind setBlendingMode:NSVisualEffectBlendingModeBehindWindow]; + [_blurBehind setMaterial:NSVisualEffectMaterialLight]; + [_blurBehind setWantsLayer:true]; + _blurBehind.hidden = true; + + [self addSubview:_blurBehind]; + [self addSubview:_content]; + + [self setWantsLayer:true]; + return self; +} + +-(void) ShowBlur:(bool)show +{ + _blurBehind.hidden = !show; +} + +-(void) ShowTitleBar: (bool) show +{ + _titleBarMaterial.hidden = !show; + _titleBarUnderline.hidden = !show; +} + +-(void) SetTitleBarHeightHint: (double) height +{ + _titleBarHeightHint = height; + + [self setFrameSize:self.frame.size]; +} + -(void)setFrameSize:(NSSize)newSize { - [super setFrameSize:newSize]; - if([[self subviews] count] == 0) + if(_settingSize) + { return; - [[self subviews][0] setFrameSize: newSize]; + } + + _settingSize = true; + [super setFrameSize:newSize]; + + [_blurBehind setFrameSize:newSize]; + [_content setFrameSize:newSize]; + + auto window = objc_cast([self window]); + + // TODO get actual titlebar size + + double height = _titleBarHeightHint == -1 ? [window getExtendedTitleBarHeight] : _titleBarHeightHint; + + NSRect tbar; + tbar.origin.x = 0; + tbar.origin.y = newSize.height - height; + tbar.size.width = newSize.width; + tbar.size.height = height; + + [_titleBarMaterial setFrame:tbar]; + tbar.size.height = height < 1 ? 0 : 1; + [_titleBarUnderline setFrame:tbar]; + _settingSize = false; +} + +-(void) SetContent: (NSView* _Nonnull) content +{ + if(content != nullptr) + { + [content removeFromSuperview]; + [self addSubview:content]; + _content = content; + } } @end @@ -1523,15 +1733,43 @@ NSArray* AllLoopModes = [NSArray arrayWithObjects: NSDefaultRunLoopMode, NSEvent bool _canBecomeKeyAndMain; bool _closed; bool _isEnabled; + bool _isExtended; AvnMenu* _menu; double _lastScaling; } +-(void) setIsExtended:(bool)value; +{ + _isExtended = value; +} + -(double) getScaling { return _lastScaling; } +-(double) getExtendedTitleBarHeight +{ + if(_isExtended) + { + for (id subview in self.contentView.superview.subviews) + { + if ([subview isKindOfClass:NSClassFromString(@"NSTitlebarContainerView")]) + { + NSView *titlebarView = [subview subviews][0]; + + return (double)titlebarView.frame.size.height; + } + } + + return -1; + } + else + { + return 0; + } +} + +(void)closeAll { NSArray* windows = [NSArray arrayWithArray:[NSApp windows]]; @@ -1650,6 +1888,7 @@ NSArray* AllLoopModes = [NSArray arrayWithObjects: NSDefaultRunLoopMode, NSEvent [self setOpaque:NO]; [self setBackgroundColor: [NSColor clearColor]]; [self invalidateShadow]; + _isExtended = false; return self; } diff --git a/samples/ControlCatalog/MainView.xaml b/samples/ControlCatalog/MainView.xaml index 0fe952e78b..c1bd740ab9 100644 --- a/samples/ControlCatalog/MainView.xaml +++ b/samples/ControlCatalog/MainView.xaml @@ -67,6 +67,7 @@ + diff --git a/samples/ControlCatalog/MainView.xaml.cs b/samples/ControlCatalog/MainView.xaml.cs index d6d4a71ad3..b0c205246e 100644 --- a/samples/ControlCatalog/MainView.xaml.cs +++ b/samples/ControlCatalog/MainView.xaml.cs @@ -58,13 +58,6 @@ 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 1ff23d1233..13fd4a2165 100644 --- a/samples/ControlCatalog/MainWindow.xaml +++ b/samples/ControlCatalog/MainWindow.xaml @@ -7,8 +7,13 @@ xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:vm="clr-namespace:ControlCatalog.ViewModels" xmlns:v="clr-namespace:ControlCatalog.Views" - TransparencyLevelHint="AcrylicBlur" - x:Class="ControlCatalog.MainWindow" WindowState="{Binding WindowState, Mode=TwoWay}" Background="Transparent"> + ExtendClientAreaToDecorationsHint="{Binding ExtendClientAreaEnabled}" + ExtendClientAreaChromeHints="{Binding ChromeHints}" + ExtendClientAreaTitleBarHeightHint="{Binding TitleBarHeight}" + TransparencyLevelHint="{Binding TransparencyLevel}" + TransparencyBackgroundFallback="Transparent" + x:Name="MainWindow" + x:Class="ControlCatalog.MainWindow" WindowState="{Binding WindowState, Mode=TwoWay}" Background="{x:Null}"> @@ -57,20 +62,30 @@ - + - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + diff --git a/samples/ControlCatalog/Pages/SliderPage.xaml b/samples/ControlCatalog/Pages/SliderPage.xaml index c6f5521e60..ea31ed0050 100644 --- a/samples/ControlCatalog/Pages/SliderPage.xaml +++ b/samples/ControlCatalog/Pages/SliderPage.xaml @@ -6,11 +6,22 @@ A control that lets the user select from a range of values by moving a Thumb control along a Track. - + + + + + + + + + + + None + Transparent + Blur + AcrylicBlur + + + diff --git a/samples/ControlCatalog/Pages/WindowCustomizationsPage.xaml.cs b/samples/ControlCatalog/Pages/WindowCustomizationsPage.xaml.cs new file mode 100644 index 0000000000..d8d4f3f371 --- /dev/null +++ b/samples/ControlCatalog/Pages/WindowCustomizationsPage.xaml.cs @@ -0,0 +1,19 @@ +using Avalonia; +using Avalonia.Controls; +using Avalonia.Markup.Xaml; + +namespace ControlCatalog.Pages +{ + public class WindowCustomizationsPage : UserControl + { + public WindowCustomizationsPage() + { + this.InitializeComponent(); + } + + private void InitializeComponent() + { + AvaloniaXamlLoader.Load(this); + } + } +} diff --git a/samples/ControlCatalog/ViewModels/MainWindowViewModel.cs b/samples/ControlCatalog/ViewModels/MainWindowViewModel.cs index 0257b4ce66..4356a032fa 100644 --- a/samples/ControlCatalog/ViewModels/MainWindowViewModel.cs +++ b/samples/ControlCatalog/ViewModels/MainWindowViewModel.cs @@ -3,6 +3,8 @@ using Avalonia.Controls; using Avalonia.Controls.ApplicationLifetimes; using Avalonia.Controls.Notifications; using Avalonia.Dialogs; +using Avalonia.Platform; +using System; using ReactiveUI; namespace ControlCatalog.ViewModels @@ -14,6 +16,12 @@ namespace ControlCatalog.ViewModels private bool _isMenuItemChecked = true; private WindowState _windowState; private WindowState[] _windowStates; + private int _transparencyLevel; + private ExtendClientAreaChromeHints _chromeHints; + private bool _extendClientAreaEnabled; + private bool _systemTitleBarEnabled; + private bool _preferSystemChromeEnabled; + private double _titleBarHeight; public MainWindowViewModel(IManagedNotificationManager notificationManager) { @@ -62,6 +70,63 @@ namespace ControlCatalog.ViewModels WindowState.Maximized, WindowState.FullScreen, }; + + this.WhenAnyValue(x => x.SystemTitleBarEnabled, x=>x.PreferSystemChromeEnabled) + .Subscribe(x => + { + var hints = ExtendClientAreaChromeHints.NoChrome | ExtendClientAreaChromeHints.OSXThickTitleBar; + + if(x.Item1) + { + hints |= ExtendClientAreaChromeHints.SystemChrome; + } + + if(x.Item2) + { + hints |= ExtendClientAreaChromeHints.PreferSystemChrome; + } + + ChromeHints = hints; + }); + + SystemTitleBarEnabled = true; + TitleBarHeight = -1; + } + + public int TransparencyLevel + { + get { return _transparencyLevel; } + set { this.RaiseAndSetIfChanged(ref _transparencyLevel, value); } + } + + public ExtendClientAreaChromeHints ChromeHints + { + get { return _chromeHints; } + set { this.RaiseAndSetIfChanged(ref _chromeHints, value); } + } + + public bool ExtendClientAreaEnabled + { + get { return _extendClientAreaEnabled; } + set { this.RaiseAndSetIfChanged(ref _extendClientAreaEnabled, value); } + } + + public bool SystemTitleBarEnabled + { + get { return _systemTitleBarEnabled; } + set { this.RaiseAndSetIfChanged(ref _systemTitleBarEnabled, value); } + } + + public bool PreferSystemChromeEnabled + { + get { return _preferSystemChromeEnabled; } + set { this.RaiseAndSetIfChanged(ref _preferSystemChromeEnabled, value); } + } + + public double TitleBarHeight + { + get { return _titleBarHeight; } + set { this.RaiseAndSetIfChanged(ref _titleBarHeight, value); } } public WindowState WindowState diff --git a/src/Avalonia.Base/AvaloniaProperty.cs b/src/Avalonia.Base/AvaloniaProperty.cs index daa7191cc5..09480f2701 100644 --- a/src/Avalonia.Base/AvaloniaProperty.cs +++ b/src/Avalonia.Base/AvaloniaProperty.cs @@ -159,8 +159,6 @@ namespace Avalonia /// internal int Id { get; } - internal bool HasChangedSubscriptions => _changed?.HasObservers ?? false; - /// /// Provides access to a property's binding via the /// indexer. @@ -512,7 +510,7 @@ namespace Avalonia /// /// An if setting the property can be undone, otherwise null. /// - internal abstract IDisposable? RouteSetValue( + internal abstract IDisposable RouteSetValue( IAvaloniaObject o, object value, BindingPriority priority); diff --git a/src/Avalonia.Base/AvaloniaPropertyRegistry.cs b/src/Avalonia.Base/AvaloniaPropertyRegistry.cs index 4a3b104f2a..4cde965400 100644 --- a/src/Avalonia.Base/AvaloniaPropertyRegistry.cs +++ b/src/Avalonia.Base/AvaloniaPropertyRegistry.cs @@ -362,7 +362,7 @@ namespace Avalonia /// The property. /// /// You won't usually want to call this method directly, instead use the - /// + /// /// method. /// public void Register(Type type, AvaloniaProperty property) @@ -413,7 +413,7 @@ namespace Avalonia /// The property. /// /// You won't usually want to call this method directly, instead use the - /// + /// /// method. /// public void RegisterAttached(Type type, AvaloniaProperty property) diff --git a/src/Avalonia.Base/Collections/Pooled/IReadOnlyPooledList.cs b/src/Avalonia.Base/Collections/Pooled/IReadOnlyPooledList.cs index 9bc3609dc5..7a233a62ab 100644 --- a/src/Avalonia.Base/Collections/Pooled/IReadOnlyPooledList.cs +++ b/src/Avalonia.Base/Collections/Pooled/IReadOnlyPooledList.cs @@ -13,9 +13,11 @@ namespace Avalonia.Collections.Pooled public interface IReadOnlyPooledList : IReadOnlyList { +#pragma warning disable CS0419 /// /// Gets a for the items currently in the collection. /// +#pragma warning restore CS0419 ReadOnlySpan Span { get; } } } diff --git a/src/Avalonia.Base/Collections/Pooled/PooledList.cs b/src/Avalonia.Base/Collections/Pooled/PooledList.cs index f0d6b292cc..e50e100d32 100644 --- a/src/Avalonia.Base/Collections/Pooled/PooledList.cs +++ b/src/Avalonia.Base/Collections/Pooled/PooledList.cs @@ -138,7 +138,6 @@ namespace Avalonia.Collections.Pooled /// initially empty, but will have room for the given number of elements /// before any reallocations are required. /// - /// If true, Count of list equals capacity. Depending on ClearMode, rented items may or may not hold dirty values. public PooledList(int capacity, ClearMode clearMode, ArrayPool customPool, bool sizeToCapacity) { if (capacity < 0) @@ -499,11 +498,13 @@ namespace Avalonia.Collections.Pooled public void AddRange(T[] array) => AddRange(array.AsSpan()); +#pragma warning disable CS0419 /// /// Adds the elements of the given to the end of this list. If /// required, the capacity of the list is increased to twice the previous /// capacity or the new size, whichever is larger. /// +#pragma warning restore CS0419 public void AddRange(ReadOnlySpan span) { var newSpan = InsertSpan(_size, span.Length, false); diff --git a/src/Avalonia.Base/Data/BindingValue.cs b/src/Avalonia.Base/Data/BindingValue.cs index 9aac1bacba..6e3c9ae67b 100644 --- a/src/Avalonia.Base/Data/BindingValue.cs +++ b/src/Avalonia.Base/Data/BindingValue.cs @@ -1,4 +1,5 @@ using System; +using System.Diagnostics.CodeAnalysis; using Avalonia.Utilities; #nullable enable @@ -81,14 +82,14 @@ namespace Avalonia.Data /// public readonly struct BindingValue { - private readonly T _value; + [AllowNull] private readonly T _value; /// /// Initializes a new instance of the struct with a type of /// /// /// The value. - public BindingValue(T value) + public BindingValue([AllowNull] T value) { ValidateValue(value); _value = value; @@ -96,7 +97,7 @@ namespace Avalonia.Data Error = null; } - private BindingValue(BindingValueType type, T value, Exception? error) + private BindingValue(BindingValueType type, [AllowNull] T value, Exception? error) { _value = value; Type = type; @@ -154,7 +155,7 @@ namespace Avalonia.Data BindingValueType.UnsetValue => AvaloniaProperty.UnsetValue, BindingValueType.DoNothing => BindingOperations.DoNothing, BindingValueType.Value => _value, - BindingValueType.BindingError => + BindingValueType.BindingError => new BindingNotification(Error, BindingErrorType.Error), BindingValueType.BindingErrorWithFallback => new BindingNotification(Error, BindingErrorType.Error, Value), @@ -175,7 +176,7 @@ namespace Avalonia.Data /// The binding type is or /// . /// - public BindingValue WithValue(T value) + public BindingValue WithValue([AllowNull] T value) { if (Type == BindingValueType.DoNothing) { @@ -190,6 +191,7 @@ namespace Avalonia.Data /// Gets the value of the binding value if present, otherwise the default value. /// /// The value. + [return: MaybeNull] public T GetValueOrDefault() => HasValue ? _value : default; /// @@ -206,6 +208,7 @@ namespace Avalonia.Data /// The value if present and of the correct type, `default(TResult)` if the value is /// not present or of an incorrect type. /// + [return: MaybeNull] public TResult GetValueOrDefault() { return HasValue ? @@ -222,7 +225,8 @@ namespace Avalonia.Data /// present but not of the correct type or null, or if the /// value is not present. /// - public TResult GetValueOrDefault(TResult defaultValue) + [return: MaybeNull] + public TResult GetValueOrDefault([AllowNull] TResult defaultValue) { return HasValue ? _value is TResult result ? result : default @@ -242,7 +246,7 @@ namespace Avalonia.Data UnsetValueType _ => Unset, DoNothingType _ => DoNothing, BindingNotification n => n.ToBindingValue().Cast(), - _ => (T)value + _ => new BindingValue((T)value) }; } @@ -250,7 +254,7 @@ namespace Avalonia.Data /// Creates a binding value from an instance of the underlying value type. /// /// The value. - public static implicit operator BindingValue(T value) => new BindingValue(value); + public static implicit operator BindingValue([AllowNull] T value) => new BindingValue(value); /// /// Creates a binding value from an . @@ -278,7 +282,7 @@ namespace Avalonia.Data /// The binding error. public static BindingValue BindingError(Exception e) { - e = e ?? throw new ArgumentNullException("e"); + e = e ?? throw new ArgumentNullException(nameof(e)); return new BindingValue(BindingValueType.BindingError, default, e); } @@ -290,7 +294,7 @@ namespace Avalonia.Data /// The fallback value. public static BindingValue BindingError(Exception e, T fallbackValue) { - e = e ?? throw new ArgumentNullException("e"); + e = e ?? throw new ArgumentNullException(nameof(e)); return new BindingValue(BindingValueType.BindingErrorWithFallback, fallbackValue, e); } @@ -303,7 +307,7 @@ namespace Avalonia.Data /// The fallback value. public static BindingValue BindingError(Exception e, Optional fallbackValue) { - e = e ?? throw new ArgumentNullException("e"); + e = e ?? throw new ArgumentNullException(nameof(e)); return new BindingValue( fallbackValue.HasValue ? @@ -319,7 +323,7 @@ namespace Avalonia.Data /// The data validation error. public static BindingValue DataValidationError(Exception e) { - e = e ?? throw new ArgumentNullException("e"); + e = e ?? throw new ArgumentNullException(nameof(e)); return new BindingValue(BindingValueType.DataValidationError, default, e); } @@ -331,7 +335,7 @@ namespace Avalonia.Data /// The fallback value. public static BindingValue DataValidationError(Exception e, T fallbackValue) { - e = e ?? throw new ArgumentNullException("e"); + e = e ?? throw new ArgumentNullException(nameof(e)); return new BindingValue(BindingValueType.DataValidationErrorWithFallback, fallbackValue, e); } @@ -344,7 +348,7 @@ namespace Avalonia.Data /// The fallback value. public static BindingValue DataValidationError(Exception e, Optional fallbackValue) { - e = e ?? throw new ArgumentNullException("e"); + e = e ?? throw new ArgumentNullException(nameof(e)); return new BindingValue( fallbackValue.HasValue ? @@ -354,7 +358,7 @@ namespace Avalonia.Data e); } - private static void ValidateValue(T value) + private static void ValidateValue([AllowNull] T value) { if (value is UnsetValueType) { diff --git a/src/Avalonia.Base/Data/Core/LogicalNotNode.cs b/src/Avalonia.Base/Data/Core/LogicalNotNode.cs index 7c402f42f6..6d09befeba 100644 --- a/src/Avalonia.Base/Data/Core/LogicalNotNode.cs +++ b/src/Avalonia.Base/Data/Core/LogicalNotNode.cs @@ -12,8 +12,19 @@ namespace Avalonia.Data.Core base.NextValueChanged(Negate(value)); } - private static object Negate(object v) + private static object Negate(object value) { + var notification = value as BindingNotification; + var v = BindingNotification.ExtractValue(value); + + BindingNotification GenerateError(Exception e) + { + notification ??= new BindingNotification(AvaloniaProperty.UnsetValue); + notification.AddError(e, BindingErrorType.Error); + notification.ClearValue(); + return notification; + } + if (v != AvaloniaProperty.UnsetValue) { var s = v as string; @@ -28,9 +39,7 @@ namespace Avalonia.Data.Core } else { - return new BindingNotification( - new InvalidCastException($"Unable to convert '{s}' to bool."), - BindingErrorType.Error); + return GenerateError(new InvalidCastException($"Unable to convert '{s}' to bool.")); } } else @@ -38,24 +47,31 @@ namespace Avalonia.Data.Core try { var boolean = Convert.ToBoolean(v, CultureInfo.InvariantCulture); - return !boolean; + + if (notification is object) + { + notification.SetValue(!boolean); + return notification; + } + else + { + return !boolean; + } } catch (InvalidCastException) { // The error message here is "Unable to cast object of type 'System.Object' // to type 'System.IConvertible'" which is kinda useless so provide our own. - return new BindingNotification( - new InvalidCastException($"Unable to convert '{v}' to bool."), - BindingErrorType.Error); + return GenerateError(new InvalidCastException($"Unable to convert '{v}' to bool.")); } catch (Exception e) { - return new BindingNotification(e, BindingErrorType.Error); + return GenerateError(e); } } } - return AvaloniaProperty.UnsetValue; + return notification ?? AvaloniaProperty.UnsetValue; } public object Transform(object value) diff --git a/src/Avalonia.Base/Data/Optional.cs b/src/Avalonia.Base/Data/Optional.cs index dd952c895c..8e044d7896 100644 --- a/src/Avalonia.Base/Data/Optional.cs +++ b/src/Avalonia.Base/Data/Optional.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; #nullable enable @@ -22,13 +23,13 @@ namespace Avalonia.Data /// public readonly struct Optional : IEquatable> { - private readonly T _value; + [AllowNull] private readonly T _value; /// /// Initializes a new instance of the struct with value. /// /// The value. - public Optional(T value) + public Optional([AllowNull] T value) { _value = value; HasValue = true; @@ -48,7 +49,7 @@ namespace Avalonia.Data public T Value => HasValue ? _value : throw new InvalidOperationException("Optional has no value."); /// - public override bool Equals(object obj) => obj is Optional o && this == o; + public override bool Equals(object? obj) => obj is Optional o && this == o; /// public bool Equals(Optional other) => this == other; @@ -69,6 +70,7 @@ namespace Avalonia.Data /// Gets the value if present, otherwise the default value. /// /// The value. + [return: MaybeNull] public T GetValueOrDefault() => HasValue ? _value : default; /// @@ -85,6 +87,7 @@ namespace Avalonia.Data /// The value if present and of the correct type, `default(TResult)` if the value is /// not present or of an incorrect type. /// + [return: MaybeNull] public TResult GetValueOrDefault() { return HasValue ? @@ -101,7 +104,8 @@ namespace Avalonia.Data /// present but not of the correct type or null, or if the /// value is not present. /// - public TResult GetValueOrDefault(TResult defaultValue) + [return: MaybeNull] + public TResult GetValueOrDefault([AllowNull] TResult defaultValue) { return HasValue ? _value is TResult result ? result : default @@ -112,7 +116,7 @@ namespace Avalonia.Data /// Creates an from an instance of the underlying value type. /// /// The value. - public static implicit operator Optional(T value) => new Optional(value); + public static implicit operator Optional([AllowNull] T value) => new Optional(value); /// /// Compares two s for inequality. @@ -128,7 +132,7 @@ namespace Avalonia.Data /// The first value. /// The second value. /// True if the values are equal; otherwise false. - public static bool operator==(Optional x, Optional y) + public static bool operator ==(Optional x, Optional y) { if (!x.HasValue && !y.HasValue) { diff --git a/src/Avalonia.Base/DirectPropertyBase.cs b/src/Avalonia.Base/DirectPropertyBase.cs index 0e65379abd..d42c030245 100644 --- a/src/Avalonia.Base/DirectPropertyBase.cs +++ b/src/Avalonia.Base/DirectPropertyBase.cs @@ -120,7 +120,7 @@ namespace Avalonia return o.GetValue(this); } - internal override object RouteGetBaseValue(IAvaloniaObject o, BindingPriority maxPriority) + internal override object? RouteGetBaseValue(IAvaloniaObject o, BindingPriority maxPriority) { return o.GetValue(this); } diff --git a/src/Avalonia.Base/IStyledPropertyMetadata.cs b/src/Avalonia.Base/IStyledPropertyMetadata.cs index f567cd930c..a68b65e5e0 100644 --- a/src/Avalonia.Base/IStyledPropertyMetadata.cs +++ b/src/Avalonia.Base/IStyledPropertyMetadata.cs @@ -1,5 +1,3 @@ -using System; - namespace Avalonia { /// diff --git a/src/Avalonia.Base/Metadata/NullableAttributes.cs b/src/Avalonia.Base/Metadata/NullableAttributes.cs new file mode 100644 index 0000000000..91f5e81863 --- /dev/null +++ b/src/Avalonia.Base/Metadata/NullableAttributes.cs @@ -0,0 +1,140 @@ +#pragma warning disable MA0048 // File name must match type name +#define INTERNAL_NULLABLE_ATTRIBUTES +#if NETSTANDARD2_0 || NETCOREAPP2_0 || NETCOREAPP2_1 || NETCOREAPP2_2 || NET45 || NET451 || NET452 || NET6 || NET461 || NET462 || NET47 || NET471 || NET472 || NET48 + +// https://github.com/dotnet/corefx/blob/48363ac826ccf66fbe31a5dcb1dc2aab9a7dd768/src/Common/src/CoreLib/System/Diagnostics/CodeAnalysis/NullableAttributes.cs + +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +namespace System.Diagnostics.CodeAnalysis +{ + /// Specifies that null is allowed as an input even if the corresponding type disallows it. + [AttributeUsage(AttributeTargets.Field | AttributeTargets.Parameter | AttributeTargets.Property, Inherited = false)] +#if INTERNAL_NULLABLE_ATTRIBUTES + internal +#else + public +#endif + sealed class AllowNullAttribute : Attribute + { } + + /// Specifies that null is disallowed as an input even if the corresponding type allows it. + [AttributeUsage(AttributeTargets.Field | AttributeTargets.Parameter | AttributeTargets.Property, Inherited = false)] +#if INTERNAL_NULLABLE_ATTRIBUTES + internal +#else + public +#endif + sealed class DisallowNullAttribute : Attribute + { } + + /// Specifies that an output may be null even if the corresponding type disallows it. + [AttributeUsage(AttributeTargets.Field | AttributeTargets.Parameter | AttributeTargets.Property | AttributeTargets.ReturnValue, Inherited = false)] +#if INTERNAL_NULLABLE_ATTRIBUTES + internal +#else + public +#endif + sealed class MaybeNullAttribute : Attribute + { } + + /// Specifies that an output will not be null even if the corresponding type allows it. + [AttributeUsage(AttributeTargets.Field | AttributeTargets.Parameter | AttributeTargets.Property | AttributeTargets.ReturnValue, Inherited = false)] +#if INTERNAL_NULLABLE_ATTRIBUTES + internal +#else + public +#endif + sealed class NotNullAttribute : Attribute + { } + + /// Specifies that when a method returns , the parameter may be null even if the corresponding type disallows it. + [AttributeUsage(AttributeTargets.Parameter, Inherited = false)] +#if INTERNAL_NULLABLE_ATTRIBUTES + internal +#else + public +#endif + sealed class MaybeNullWhenAttribute : Attribute + { + /// Initializes the attribute with the specified return value condition. + /// + /// The return value condition. If the method returns this value, the associated parameter may be null. + /// + public MaybeNullWhenAttribute(bool returnValue) => ReturnValue = returnValue; + + /// Gets the return value condition. + public bool ReturnValue { get; } + } + + /// Specifies that when a method returns , the parameter will not be null even if the corresponding type allows it. + [AttributeUsage(AttributeTargets.Parameter, Inherited = false)] +#if INTERNAL_NULLABLE_ATTRIBUTES + internal +#else + public +#endif + sealed class NotNullWhenAttribute : Attribute + { + /// Initializes the attribute with the specified return value condition. + /// + /// The return value condition. If the method returns this value, the associated parameter will not be null. + /// + public NotNullWhenAttribute(bool returnValue) => ReturnValue = returnValue; + + /// Gets the return value condition. + public bool ReturnValue { get; } + } + + /// Specifies that the output will be non-null if the named parameter is non-null. + [AttributeUsage(AttributeTargets.Parameter | AttributeTargets.Property | AttributeTargets.ReturnValue, AllowMultiple = true, Inherited = false)] +#if INTERNAL_NULLABLE_ATTRIBUTES + internal +#else + public +#endif + sealed class NotNullIfNotNullAttribute : Attribute + { + /// Initializes the attribute with the associated parameter name. + /// + /// The associated parameter name. The output will be non-null if the argument to the parameter specified is non-null. + /// + public NotNullIfNotNullAttribute(string parameterName) => ParameterName = parameterName; + + /// Gets the associated parameter name. + public string ParameterName { get; } + } + + /// Applied to a method that will never return under any circumstance. + [AttributeUsage(AttributeTargets.Method, Inherited = false)] +#if INTERNAL_NULLABLE_ATTRIBUTES + internal +#else + public +#endif + sealed class DoesNotReturnAttribute : Attribute + { } + + /// Specifies that the method will not return if the associated Boolean parameter is passed the specified value. + [AttributeUsage(AttributeTargets.Parameter, Inherited = false)] +#if INTERNAL_NULLABLE_ATTRIBUTES + internal +#else + public +#endif + sealed class DoesNotReturnIfAttribute : Attribute + { + /// Initializes the attribute with the specified parameter value. + /// + /// The condition parameter value. Code after the method will be considered unreachable by diagnostics if the argument to + /// the associated parameter matches this value. + /// + public DoesNotReturnIfAttribute(bool parameterValue) => ParameterValue = parameterValue; + + /// Gets the condition parameter value. + public bool ParameterValue { get; } + } +} +#endif diff --git a/src/Avalonia.Base/PropertyStore/IPriorityValueEntry.cs b/src/Avalonia.Base/PropertyStore/IPriorityValueEntry.cs index 6ed6c2ef52..4d82381323 100644 --- a/src/Avalonia.Base/PropertyStore/IPriorityValueEntry.cs +++ b/src/Avalonia.Base/PropertyStore/IPriorityValueEntry.cs @@ -1,7 +1,4 @@ -using System; -using Avalonia.Data; - -#nullable enable +#nullable enable namespace Avalonia.PropertyStore { @@ -10,8 +7,6 @@ namespace Avalonia.PropertyStore /// internal interface IPriorityValueEntry : IValue { - BindingPriority Priority { get; } - void Reparent(IValueSink sink); } diff --git a/src/Avalonia.Base/PropertyStore/LocalValueEntry.cs b/src/Avalonia.Base/PropertyStore/LocalValueEntry.cs index 59c017bc09..859e9ba81c 100644 --- a/src/Avalonia.Base/PropertyStore/LocalValueEntry.cs +++ b/src/Avalonia.Base/PropertyStore/LocalValueEntry.cs @@ -1,4 +1,5 @@ -using Avalonia.Data; +using System.Diagnostics.CodeAnalysis; +using Avalonia.Data; #nullable enable @@ -11,9 +12,9 @@ namespace Avalonia.PropertyStore /// The property type. internal class LocalValueEntry : IValue { - private T _value; + [AllowNull] private T _value; - public LocalValueEntry(T value) => _value = value; + public LocalValueEntry([AllowNull] T value) => _value = value; public BindingPriority Priority => BindingPriority.LocalValue; Optional IValue.GetValue() => new Optional(_value); diff --git a/src/Avalonia.Base/StyledPropertyMetadata`1.cs b/src/Avalonia.Base/StyledPropertyMetadata`1.cs index 300548db0a..cf0a0c34ec 100644 --- a/src/Avalonia.Base/StyledPropertyMetadata`1.cs +++ b/src/Avalonia.Base/StyledPropertyMetadata`1.cs @@ -1,5 +1,4 @@ using System; -using System.Diagnostics; using Avalonia.Data; namespace Avalonia @@ -35,7 +34,7 @@ namespace Avalonia /// /// Gets the value coercion callback, if any. /// - public Func? CoerceValue { get; private set; } + public Func CoerceValue { get; private set; } object IStyledPropertyMetadata.DefaultValue => DefaultValue; diff --git a/src/Avalonia.Base/Utilities/StyleClassParser.cs b/src/Avalonia.Base/Utilities/StyleClassParser.cs new file mode 100644 index 0000000000..2db58f73d9 --- /dev/null +++ b/src/Avalonia.Base/Utilities/StyleClassParser.cs @@ -0,0 +1,45 @@ +using System; +using System.Globalization; + +namespace Avalonia.Utilities +{ +#if !BUILDTASK + public +#endif + static class StyleClassParser + { + public static ReadOnlySpan ParseStyleClass(this ref CharacterReader r) + { + if (IsValidIdentifierStart(r.Peek)) + { + return r.TakeWhile(c => IsValidIdentifierChar(c)); + } + else + { + return ReadOnlySpan.Empty; + } + } + + private static bool IsValidIdentifierStart(char c) + { + return char.IsLetter(c) || c == '_'; + } + + private static bool IsValidIdentifierChar(char c) + { + if (IsValidIdentifierStart(c) || c == '-') + { + return true; + } + else + { + var cat = CharUnicodeInfo.GetUnicodeCategory(c); + return cat == UnicodeCategory.NonSpacingMark || + cat == UnicodeCategory.SpacingCombiningMark || + cat == UnicodeCategory.ConnectorPunctuation || + cat == UnicodeCategory.Format || + cat == UnicodeCategory.DecimalDigitNumber; + } + } + } +} diff --git a/src/Avalonia.Base/ValueStore.cs b/src/Avalonia.Base/ValueStore.cs index 05e66f2e0a..6b89fcbdb9 100644 --- a/src/Avalonia.Base/ValueStore.cs +++ b/src/Avalonia.Base/ValueStore.cs @@ -162,7 +162,7 @@ namespace Avalonia _sink.ValueChanged(new AvaloniaPropertyChangedEventArgs( _owner, property, - old, + new Optional(old), default, BindingPriority.Unset)); } diff --git a/src/Avalonia.Build.Tasks/Avalonia.Build.Tasks.csproj b/src/Avalonia.Build.Tasks/Avalonia.Build.Tasks.csproj index 582e4499c5..5b2484382e 100644 --- a/src/Avalonia.Build.Tasks/Avalonia.Build.Tasks.csproj +++ b/src/Avalonia.Build.Tasks/Avalonia.Build.Tasks.csproj @@ -42,7 +42,10 @@ Markup/%(RecursiveDir)%(FileName)%(Extension) - + + Markup/%(RecursiveDir)%(FileName)%(Extension) + + diff --git a/src/Avalonia.Controls/Chrome/CaptionButtons.cs b/src/Avalonia.Controls/Chrome/CaptionButtons.cs new file mode 100644 index 0000000000..75d6c366b8 --- /dev/null +++ b/src/Avalonia.Controls/Chrome/CaptionButtons.cs @@ -0,0 +1,86 @@ +using System; +using System.Reactive.Disposables; +using Avalonia.Controls.Primitives; + +#nullable enable + +namespace Avalonia.Controls.Chrome +{ + /// + /// Draws window minimize / maximize / close buttons in a when managed client decorations are enabled. + /// + public class CaptionButtons : TemplatedControl + { + private CompositeDisposable? _disposables; + private Window? _hostWindow; + + public void Attach(Window hostWindow) + { + if (_disposables == null) + { + _hostWindow = hostWindow; + + _disposables = new CompositeDisposable + { + _hostWindow.GetObservable(Window.WindowStateProperty) + .Subscribe(x => + { + PseudoClasses.Set(":minimized", x == WindowState.Minimized); + PseudoClasses.Set(":normal", x == WindowState.Normal); + PseudoClasses.Set(":maximized", x == WindowState.Maximized); + PseudoClasses.Set(":fullscreen", x == WindowState.FullScreen); + }) + }; + } + } + + public void Detach() + { + if (_disposables != null) + { + var layer = ChromeOverlayLayer.GetOverlayLayer(_hostWindow); + + layer?.Children.Remove(this); + + _disposables.Dispose(); + _disposables = null; + } + } + + protected override void OnApplyTemplate(TemplateAppliedEventArgs e) + { + base.OnApplyTemplate(e); + + var closeButton = e.NameScope.Get("PART_CloseButton"); + var restoreButton = e.NameScope.Get("PART_RestoreButton"); + var minimiseButton = e.NameScope.Get("PART_MinimiseButton"); + var fullScreenButton = e.NameScope.Get("PART_FullScreenButton"); + + closeButton.PointerReleased += (sender, e) => _hostWindow?.Close(); + + restoreButton.PointerReleased += (sender, e) => + { + if (_hostWindow != null) + { + _hostWindow.WindowState = _hostWindow.WindowState == WindowState.Maximized ? WindowState.Normal : WindowState.Maximized; + } + }; + + minimiseButton.PointerReleased += (sender, e) => + { + if (_hostWindow != null) + { + _hostWindow.WindowState = WindowState.Minimized; + } + }; + + fullScreenButton.PointerReleased += (sender, e) => + { + if (_hostWindow != null) + { + _hostWindow.WindowState = _hostWindow.WindowState == WindowState.FullScreen ? WindowState.Normal : WindowState.FullScreen; + } + }; + } + } +} diff --git a/src/Avalonia.Controls/Chrome/TitleBar.cs b/src/Avalonia.Controls/Chrome/TitleBar.cs new file mode 100644 index 0000000000..78b49d2a03 --- /dev/null +++ b/src/Avalonia.Controls/Chrome/TitleBar.cs @@ -0,0 +1,117 @@ +using System; +using System.Reactive.Disposables; +using Avalonia.Controls.Primitives; + +#nullable enable + +namespace Avalonia.Controls.Chrome +{ + /// + /// Draws a titlebar when managed client decorations are enabled. + /// + public class TitleBar : TemplatedControl + { + private CompositeDisposable? _disposables; + private readonly Window? _hostWindow; + private CaptionButtons? _captionButtons; + + public TitleBar(Window hostWindow) + { + _hostWindow = hostWindow; + } + + public TitleBar() + { + + } + + public void Attach() + { + if (_disposables == null) + { + var layer = ChromeOverlayLayer.GetOverlayLayer(_hostWindow); + + layer?.Children.Add(this); + + if (_hostWindow != null) + { + _disposables = new CompositeDisposable + { + _hostWindow.GetObservable(Window.WindowDecorationMarginProperty) + .Subscribe(x => UpdateSize()), + + _hostWindow.GetObservable(Window.ExtendClientAreaTitleBarHeightHintProperty) + .Subscribe(x => UpdateSize()), + + _hostWindow.GetObservable(Window.OffScreenMarginProperty) + .Subscribe(x => UpdateSize()), + + _hostWindow.GetObservable(Window.WindowStateProperty) + .Subscribe(x => + { + PseudoClasses.Set(":minimized", x == WindowState.Minimized); + PseudoClasses.Set(":normal", x == WindowState.Normal); + PseudoClasses.Set(":maximized", x == WindowState.Maximized); + PseudoClasses.Set(":fullscreen", x == WindowState.FullScreen); + }) + }; + + _captionButtons?.Attach(_hostWindow); + } + + UpdateSize(); + } + } + + private void UpdateSize() + { + if (_hostWindow != null) + { + Margin = new Thickness( + _hostWindow.OffScreenMargin.Left, + _hostWindow.OffScreenMargin.Top, + _hostWindow.OffScreenMargin.Right, + _hostWindow.OffScreenMargin.Bottom); + + if (_hostWindow.WindowState != WindowState.FullScreen) + { + Height = _hostWindow.WindowDecorationMargin.Top; + + if (_captionButtons != null) + { + _captionButtons.Height = Height; + } + } + } + } + + public void Detach() + { + if (_disposables != null) + { + var layer = ChromeOverlayLayer.GetOverlayLayer(_hostWindow); + + layer?.Children.Remove(this); + + _disposables.Dispose(); + _disposables = null; + + _captionButtons?.Detach(); + } + } + + protected override void OnApplyTemplate(TemplateAppliedEventArgs e) + { + base.OnApplyTemplate(e); + + _captionButtons = e.NameScope.Get("PART_CaptionButtons"); + + if (_hostWindow != null) + { + _captionButtons.Attach(_hostWindow); + } + + UpdateSize(); + } + } +} diff --git a/src/Avalonia.Controls/Platform/ExtendClientAreaChromeHints.cs b/src/Avalonia.Controls/Platform/ExtendClientAreaChromeHints.cs new file mode 100644 index 0000000000..de3f58886b --- /dev/null +++ b/src/Avalonia.Controls/Platform/ExtendClientAreaChromeHints.cs @@ -0,0 +1,38 @@ +using System; + +namespace Avalonia.Platform +{ + /// + /// Hint for Window Chrome when ClientArea is Extended. + /// + [Flags] + public enum ExtendClientAreaChromeHints + { + /// + /// The will be no chrome at all. + /// + NoChrome, + + /// + /// The default for the platform. + /// + Default = SystemChrome, + + /// + /// Use SystemChrome + /// + SystemChrome = 0x01, + + /// + /// Use system chrome where possible. OSX system chrome is used, Windows managed chrome is used. + /// This is because Windows Chrome can not be shown ontop of user content. + /// + PreferSystemChrome = 0x02, + + /// + /// On OSX the titlebar is the thicker toolbar kind. Causes traffic lights to be positioned + /// slightly lower than normal. + /// + OSXThickTitleBar = 0x08, + } +} diff --git a/src/Avalonia.Controls/Platform/IWindowImpl.cs b/src/Avalonia.Controls/Platform/IWindowImpl.cs index cf31d30332..8a1554d344 100644 --- a/src/Avalonia.Controls/Platform/IWindowImpl.cs +++ b/src/Avalonia.Controls/Platform/IWindowImpl.cs @@ -68,6 +68,34 @@ namespace Avalonia.Platform /// Func Closing { get; set; } + /// + /// Gets a value to indicate if the platform was able to extend client area to non-client area. + /// + bool IsClientAreaExtendedToDecorations { get; } + + /// + /// Gets or Sets an action that is called whenever one of the extend client area properties changed. + /// + Action ExtendClientAreaToDecorationsChanged { get; set; } + + /// + /// Gets a flag that indicates if Managed decorations i.e. caption buttons are required. + /// This property is used when is set. + /// + bool NeedsManagedDecorations { get; } + + /// + /// Gets a thickness that describes the amount each side of the non-client area extends into the client area. + /// It includes the titlebar. + /// + Thickness ExtendedMargins { get; } + + /// + /// Gets a thickness that describes the margin around the window that is offscreen. + /// This may happen when a window is maximized and is set. + /// + Thickness OffScreenMargin { get; } + /// /// Starts moving a window with left button being held. Should be called from left mouse button press event handler. /// @@ -94,5 +122,23 @@ namespace Avalonia.Platform /// /// void SetMinMaxSize(Size minSize, Size maxSize); + + /// + /// Sets if the ClientArea is extended into the non-client area. + /// + /// true to enable, false to disable + void SetExtendClientAreaToDecorationsHint(bool extendIntoClientAreaHint); + + /// + /// Sets hints that configure how the client area extends. + /// + /// + void SetExtendClientAreaChromeHints(ExtendClientAreaChromeHints hints); + + /// + /// Sets how big the non-client titlebar area should be. + /// + /// -1 for platform default, otherwise the height in DIPs. + void SetExtendClientAreaTitleBarHeightHint(double titleBarHeight); } } diff --git a/src/Avalonia.Controls/Primitives/ChromeOverlayLayer.cs b/src/Avalonia.Controls/Primitives/ChromeOverlayLayer.cs new file mode 100644 index 0000000000..ba0fdfd535 --- /dev/null +++ b/src/Avalonia.Controls/Primitives/ChromeOverlayLayer.cs @@ -0,0 +1,29 @@ +using System.Linq; +using Avalonia.Rendering; +using Avalonia.VisualTree; + +#nullable enable + +namespace Avalonia.Controls.Primitives +{ + public class ChromeOverlayLayer : Panel, ICustomSimpleHitTest + { + public static ChromeOverlayLayer? GetOverlayLayer(IVisual visual) + { + foreach (var v in visual.GetVisualAncestors()) + if (v is VisualLayerManager vlm) + if (vlm.OverlayLayer != null) + return vlm.ChromeOverlayLayer; + + if (visual is TopLevel tl) + { + var layers = tl.GetVisualDescendants().OfType().FirstOrDefault(); + return layers?.ChromeOverlayLayer; + } + + return null; + } + + public bool HitTest(Point point) => Children.HitTestCustom(point); + } +} diff --git a/src/Avalonia.Controls/Primitives/VisualLayerManager.cs b/src/Avalonia.Controls/Primitives/VisualLayerManager.cs index 86aeb5c62a..3084d7fa72 100644 --- a/src/Avalonia.Controls/Primitives/VisualLayerManager.cs +++ b/src/Avalonia.Controls/Primitives/VisualLayerManager.cs @@ -6,7 +6,9 @@ namespace Avalonia.Controls.Primitives public class VisualLayerManager : Decorator { private const int AdornerZIndex = int.MaxValue - 100; - private const int OverlayZIndex = int.MaxValue - 99; + private const int ChromeZIndex = int.MaxValue - 99; + private const int OverlayZIndex = int.MaxValue - 98; + private ILogicalRoot _logicalRoot; private readonly List _layers = new List(); @@ -24,6 +26,17 @@ namespace Avalonia.Controls.Primitives } } + public ChromeOverlayLayer ChromeOverlayLayer + { + get + { + var rv = FindLayer(); + if (rv == null) + AddLayer(rv = new ChromeOverlayLayer(), ChromeZIndex); + return rv; + } + } + public OverlayLayer OverlayLayer { get diff --git a/src/Avalonia.Controls/Properties/AssemblyInfo.cs b/src/Avalonia.Controls/Properties/AssemblyInfo.cs index 060db46212..672fbe294e 100644 --- a/src/Avalonia.Controls/Properties/AssemblyInfo.cs +++ b/src/Avalonia.Controls/Properties/AssemblyInfo.cs @@ -13,3 +13,4 @@ using Avalonia.Metadata; [assembly: XmlnsDefinition("https://github.com/avaloniaui", "Avalonia.Controls.Shapes")] [assembly: XmlnsDefinition("https://github.com/avaloniaui", "Avalonia.Controls.Templates")] [assembly: XmlnsDefinition("https://github.com/avaloniaui", "Avalonia.Controls.Notifications")] +[assembly: XmlnsDefinition("https://github.com/avaloniaui", "Avalonia.Controls.Chrome")] diff --git a/src/Avalonia.Controls/Slider.cs b/src/Avalonia.Controls/Slider.cs index fe1a4f5ac1..293cbac82f 100644 --- a/src/Avalonia.Controls/Slider.cs +++ b/src/Avalonia.Controls/Slider.cs @@ -1,4 +1,5 @@ using System; +using Avalonia.Collections; using Avalonia.Controls.Mixins; using Avalonia.Controls.Primitives; using Avalonia.Input; @@ -64,6 +65,12 @@ namespace Avalonia.Controls public static readonly StyledProperty TickPlacementProperty = AvaloniaProperty.Register(nameof(TickPlacement), 0d); + /// + /// Defines the property. + /// + public static readonly StyledProperty> TicksProperty = + TickBar.TicksProperty.AddOwner(); + // Slider required parts private bool _isDragging = false; private Track _track; @@ -83,7 +90,8 @@ namespace Avalonia.Controls PressedMixin.Attach(); OrientationProperty.OverrideDefaultValue(typeof(Slider), Orientation.Horizontal); Thumb.DragStartedEvent.AddClassHandler((x, e) => x.OnThumbDragStarted(e), RoutingStrategies.Bubble); - Thumb.DragCompletedEvent.AddClassHandler((x, e) => x.OnThumbDragCompleted(e), RoutingStrategies.Bubble); + Thumb.DragCompletedEvent.AddClassHandler((x, e) => x.OnThumbDragCompleted(e), + RoutingStrategies.Bubble); } /// @@ -94,6 +102,15 @@ namespace Avalonia.Controls UpdatePseudoClasses(Orientation); } + /// + /// Defines the ticks to be drawn on the tick bar. + /// + public AvaloniaList Ticks + { + get => GetValue(TicksProperty); + set => SetValue(TicksProperty, value); + } + /// /// Gets or sets the orientation of a . /// @@ -240,19 +257,50 @@ namespace Avalonia.Controls /// Value that want to snap to closest Tick. private double SnapToTick(double value) { - var previous = Minimum; - var next = Maximum; - - if (TickFrequency > 0.0) + if (IsSnapToTickEnabled) { - previous = Minimum + (Math.Round((value - Minimum) / TickFrequency) * TickFrequency); - next = Math.Min(Maximum, previous + TickFrequency); + double previous = Minimum; + double next = Maximum; + + // This property is rarely set so let's try to avoid the GetValue + var ticks = Ticks; + + // If ticks collection is available, use it. + // Note that ticks may be unsorted. + if ((ticks != null) && (ticks.Count > 0)) + { + for (int i = 0; i < ticks.Count; i++) + { + double tick = ticks[i]; + if (MathUtilities.AreClose(tick, value)) + { + return value; + } + + if (MathUtilities.LessThan(tick, value) && MathUtilities.GreaterThan(tick, previous)) + { + previous = tick; + } + else if (MathUtilities.GreaterThan(tick, value) && MathUtilities.LessThan(tick, next)) + { + next = tick; + } + } + } + else if (MathUtilities.GreaterThan(TickFrequency, 0.0)) + { + previous = Minimum + (Math.Round(((value - Minimum) / TickFrequency)) * TickFrequency); + next = Math.Min(Maximum, previous + TickFrequency); + } + + // Choose the closest value between previous and next. If tie, snap to 'next'. + value = MathUtilities.GreaterThanOrClose(value, (previous + next) * 0.5) ? next : previous; } - // Choose the closest value between previous and next. If tie, snap to 'next'. - return MathUtilities.GreaterThanOrClose(value, (previous + next) * 0.5) ? next : previous; + return value; } + private void UpdatePseudoClasses(Orientation o) { PseudoClasses.Set(":vertical", o == Orientation.Vertical); diff --git a/src/Avalonia.Controls/TickBar.cs b/src/Avalonia.Controls/TickBar.cs index 16e063beb3..22145d8742 100644 --- a/src/Avalonia.Controls/TickBar.cs +++ b/src/Avalonia.Controls/TickBar.cs @@ -1,8 +1,4 @@ -using System; -using System.Collections.Generic; -using Avalonia.Controls.Primitives; -using Avalonia.Data; -using Avalonia.Data.Converters; +using Avalonia.Collections; using Avalonia.Layout; using Avalonia.Media; using Avalonia.Utilities; @@ -135,15 +131,15 @@ namespace Avalonia.Controls /// /// Defines the property. /// - public static readonly StyledProperty> TicksProperty = - AvaloniaProperty.Register>(nameof(Ticks)); + public static readonly StyledProperty> TicksProperty = + AvaloniaProperty.Register>(nameof(Ticks)); /// /// The Ticks property contains collection of value of type Double which /// are the logical positions use to draw the ticks. /// The property value is a . /// - public List Ticks + public AvaloniaList Ticks { get { return GetValue(TicksProperty); } set { SetValue(TicksProperty, value); } diff --git a/src/Avalonia.Controls/TopLevel.cs b/src/Avalonia.Controls/TopLevel.cs index f058942116..611f0c9290 100644 --- a/src/Avalonia.Controls/TopLevel.cs +++ b/src/Avalonia.Controls/TopLevel.cs @@ -405,7 +405,7 @@ namespace Avalonia.Controls } else { - _transparencyFallbackBorder.Background = Brushes.Transparent; + _transparencyFallbackBorder.Background = null; } } diff --git a/src/Avalonia.Controls/Window.cs b/src/Avalonia.Controls/Window.cs index cedd20ace5..18d8c89f49 100644 --- a/src/Avalonia.Controls/Window.cs +++ b/src/Avalonia.Controls/Window.cs @@ -4,6 +4,7 @@ using System.ComponentModel; using System.Linq; using System.Reactive.Linq; using System.Threading.Tasks; +using Avalonia.Controls.Chrome; using Avalonia.Controls.Platform; using Avalonia.Data; using Avalonia.Input; @@ -69,7 +70,11 @@ namespace Avalonia.Controls /// public class Window : WindowBase, IStyleable, IFocusScope, ILayoutRoot { - private readonly List<(Window child, bool isDialog)> _children = new List<(Window, bool)>(); + private readonly List<(Window child, bool isDialog)> _children = new List<(Window, bool)>(); + private TitleBar _managedTitleBar; + private bool _isExtendedIntoWindowDecorations; + private Thickness _windowDecorationMargin; + private Thickness _offScreenMargin; /// /// Defines the property. @@ -87,6 +92,37 @@ namespace Avalonia.Controls o => o.HasSystemDecorations, (o, v) => o.HasSystemDecorations = v); + /// + /// Defines the property. + /// + public static readonly StyledProperty ExtendClientAreaToDecorationsHintProperty = + AvaloniaProperty.Register(nameof(ExtendClientAreaToDecorationsHint), false); + + public static readonly StyledProperty ExtendClientAreaChromeHintsProperty = + AvaloniaProperty.Register(nameof(ExtendClientAreaChromeHints), ExtendClientAreaChromeHints.Default); + + public static readonly StyledProperty ExtendClientAreaTitleBarHeightHintProperty = + AvaloniaProperty.Register(nameof(ExtendClientAreaTitleBarHeightHint), -1); + + /// + /// Defines the property. + /// + public static readonly DirectProperty IsExtendedIntoWindowDecorationsProperty = + AvaloniaProperty.RegisterDirect(nameof(IsExtendedIntoWindowDecorations), + o => o.IsExtendedIntoWindowDecorations, + unsetValue: false); + + /// + /// Defines the property. + /// + public static readonly DirectProperty WindowDecorationMarginProperty = + AvaloniaProperty.RegisterDirect(nameof(WindowDecorationMargin), + o => o.WindowDecorationMargin); + + public static readonly DirectProperty OffScreenMarginProperty = + AvaloniaProperty.RegisterDirect(nameof(OffScreenMargin), + o => o.OffScreenMargin); + /// /// Defines the property. /// @@ -164,6 +200,21 @@ namespace Avalonia.Controls WindowStateProperty.Changed.AddClassHandler( (w, e) => { if (w.PlatformImpl != null) w.PlatformImpl.WindowState = (WindowState)e.NewValue; }); + ExtendClientAreaToDecorationsHintProperty.Changed.AddClassHandler( + (w, e) => { if (w.PlatformImpl != null) w.PlatformImpl.SetExtendClientAreaToDecorationsHint((bool)e.NewValue); }); + + ExtendClientAreaChromeHintsProperty.Changed.AddClassHandler( + (w, e) => + { + if (w.PlatformImpl != null) + { + w.PlatformImpl.SetExtendClientAreaChromeHints((ExtendClientAreaChromeHints)e.NewValue); + } + }); + + ExtendClientAreaTitleBarHeightHintProperty.Changed.AddClassHandler( + (w, e) => { if (w.PlatformImpl != null) w.PlatformImpl.SetExtendClientAreaTitleBarHeightHint((double)e.NewValue); }); + MinWidthProperty.Changed.AddClassHandler((w, e) => w.PlatformImpl?.SetMinMaxSize(new Size((double)e.NewValue, w.MinHeight), new Size(w.MaxWidth, w.MaxHeight))); MinHeightProperty.Changed.AddClassHandler((w, e) => w.PlatformImpl?.SetMinMaxSize(new Size(w.MinWidth, (double)e.NewValue), new Size(w.MaxWidth, w.MaxHeight))); MaxWidthProperty.Changed.AddClassHandler((w, e) => w.PlatformImpl?.SetMinMaxSize(new Size(w.MinWidth, w.MinHeight), new Size((double)e.NewValue, w.MaxHeight))); @@ -189,6 +240,7 @@ namespace Avalonia.Controls impl.GotInputWhenDisabled = OnGotInputWhenDisabled; impl.WindowStateChanged = HandleWindowStateChanged; _maxPlatformClientSize = PlatformImpl?.MaxAutoSizeHint ?? default(Size); + impl.ExtendClientAreaToDecorationsChanged = ExtendClientAreaToDecorationsChanged; this.GetObservable(ClientSizeProperty).Skip(1).Subscribe(x => PlatformImpl?.Resize(x)); PlatformImpl?.ShowTaskbarIcon(ShowInTaskbar); @@ -237,6 +289,66 @@ namespace Avalonia.Controls } } + /// + /// Gets or sets if the ClientArea is Extended into the Window Decorations (chrome or border). + /// + public bool ExtendClientAreaToDecorationsHint + { + get { return GetValue(ExtendClientAreaToDecorationsHintProperty); } + set { SetValue(ExtendClientAreaToDecorationsHintProperty, value); } + } + + /// + /// Gets or Sets the that control + /// how the chrome looks when the client area is extended. + /// + public ExtendClientAreaChromeHints ExtendClientAreaChromeHints + { + get => GetValue(ExtendClientAreaChromeHintsProperty); + set => SetValue(ExtendClientAreaChromeHintsProperty, value); + } + + /// + /// Gets or Sets the TitlebarHeightHint for when the client area is extended. + /// A value of -1 will cause the titlebar to be auto sized to the OS default. + /// Any other positive value will cause the titlebar to assume that height. + /// + public double ExtendClientAreaTitleBarHeightHint + { + get => GetValue(ExtendClientAreaTitleBarHeightHintProperty); + set => SetValue(ExtendClientAreaTitleBarHeightHintProperty, value); + } + + /// + /// Gets if the ClientArea is Extended into the Window Decorations. + /// + public bool IsExtendedIntoWindowDecorations + { + get => _isExtendedIntoWindowDecorations; + private set => SetAndRaise(IsExtendedIntoWindowDecorationsProperty, ref _isExtendedIntoWindowDecorations, value); + } + + /// + /// Gets the WindowDecorationMargin. + /// This tells you the thickness around the window that is used by borders and the titlebar. + /// + public Thickness WindowDecorationMargin + { + get => _windowDecorationMargin; + private set => SetAndRaise(WindowDecorationMarginProperty, ref _windowDecorationMargin, value); + } + + /// + /// Gets the window margin that is hidden off the screen area. + /// This is generally only the case on Windows when in Maximized where the window border + /// is hidden off the screen. This Margin may be used to ensure user content doesnt overlap this space. + /// + public Thickness OffScreenMargin + { + get => _offScreenMargin; + private set => SetAndRaise(OffScreenMarginProperty, ref _offScreenMargin, value); + } + /// /// Sets the system decorations (title bar, border, etc) /// @@ -435,6 +547,27 @@ namespace Avalonia.Controls } } + protected virtual void ExtendClientAreaToDecorationsChanged(bool isExtended) + { + IsExtendedIntoWindowDecorations = isExtended; + WindowDecorationMargin = PlatformImpl.ExtendedMargins; + OffScreenMargin = PlatformImpl.OffScreenMargin; + + if (PlatformImpl.NeedsManagedDecorations) + { + if (_managedTitleBar == null) + { + _managedTitleBar = new TitleBar(this); + _managedTitleBar.Attach(); + } + } + else + { + _managedTitleBar?.Detach(); + _managedTitleBar = null; + } + } + /// /// Hides the window but does not close it. /// diff --git a/src/Avalonia.DesignerSupport/Remote/PreviewerWindowImpl.cs b/src/Avalonia.DesignerSupport/Remote/PreviewerWindowImpl.cs index d42eda6e5e..dce24df9d9 100644 --- a/src/Avalonia.DesignerSupport/Remote/PreviewerWindowImpl.cs +++ b/src/Avalonia.DesignerSupport/Remote/PreviewerWindowImpl.cs @@ -78,7 +78,17 @@ namespace Avalonia.DesignerSupport.Remote } public IScreenImpl Screen { get; } = new ScreenStub(); - public Action GotInputWhenDisabled { get; set; } + public Action GotInputWhenDisabled { get; set; } + + public Action ExtendClientAreaToDecorationsChanged { get; set; } + + public Thickness ExtendedMargins { get; } = new Thickness(); + + public bool IsClientAreaExtendedToDecorations { get; } + + public Thickness OffScreenMargin { get; } = new Thickness(); + + public bool NeedsManagedDecorations => false; public void Activate() { @@ -119,5 +129,17 @@ namespace Avalonia.DesignerSupport.Remote public void SetEnabled(bool enable) { } + + public void SetExtendClientAreaToDecorationsHint(bool extendIntoClientAreaHint) + { + } + + public void SetExtendClientAreaChromeHints(ExtendClientAreaChromeHints hints) + { + } + + public void SetExtendClientAreaTitleBarHeightHint(double titleBarHeight) + { + } } } diff --git a/src/Avalonia.DesignerSupport/Remote/Stubs.cs b/src/Avalonia.DesignerSupport/Remote/Stubs.cs index 39b8d7f076..84c52d6fbf 100644 --- a/src/Avalonia.DesignerSupport/Remote/Stubs.cs +++ b/src/Avalonia.DesignerSupport/Remote/Stubs.cs @@ -38,7 +38,13 @@ namespace Avalonia.DesignerSupport.Remote public WindowState WindowState { get; set; } public Action WindowStateChanged { get; set; } - public Action TransparencyLevelChanged { get; set; } + public Action TransparencyLevelChanged { get; set; } + + public Action ExtendClientAreaToDecorationsChanged { get; set; } + + public Thickness ExtendedMargins { get; } = new Thickness(); + + public Thickness OffScreenMargin { get; } = new Thickness(); public WindowStub(IWindowImpl parent = null) { @@ -141,6 +147,18 @@ namespace Avalonia.DesignerSupport.Remote { } + public void SetExtendClientAreaToDecorationsHint(bool extendIntoClientAreaHint) + { + } + + public void SetExtendClientAreaChromeHints(ExtendClientAreaChromeHints hints) + { + } + + public void SetExtendClientAreaTitleBarHeightHint(double titleBarHeight) + { + } + public IPopupPositioner PopupPositioner { get; } public Action GotInputWhenDisabled { get; set; } @@ -152,6 +170,10 @@ namespace Avalonia.DesignerSupport.Remote } public WindowTransparencyLevel TransparencyLevel { get; private set; } + + public bool IsClientAreaExtendedToDecorations { get; } + + public bool NeedsManagedDecorations => false; } class ClipboardStub : IClipboard diff --git a/src/Avalonia.Input/InputElement.cs b/src/Avalonia.Input/InputElement.cs index 407b28b665..0616c70d82 100644 --- a/src/Avalonia.Input/InputElement.cs +++ b/src/Avalonia.Input/InputElement.cs @@ -158,6 +158,7 @@ namespace Avalonia.Input private bool _isEffectivelyEnabled = true; private bool _isFocused; + private bool _isFocusVisible; private bool _isPointerOver; private GestureRecognizerCollection _gestureRecognizers; @@ -427,7 +428,9 @@ namespace Avalonia.Input /// The event args. protected virtual void OnGotFocus(GotFocusEventArgs e) { - IsFocused = e.Source == this; + var isFocused = e.Source == this; + _isFocusVisible = isFocused && (e.NavigationMethod == NavigationMethod.Directional || e.NavigationMethod == NavigationMethod.Tab); + IsFocused = isFocused; } /// @@ -436,6 +439,7 @@ namespace Avalonia.Input /// The event args. protected virtual void OnLostFocus(RoutedEventArgs e) { + _isFocusVisible = false; IsFocused = false; } @@ -602,6 +606,7 @@ namespace Avalonia.Input if (isFocused.HasValue) { PseudoClasses.Set(":focus", isFocused.Value); + PseudoClasses.Set(":focus-visible", _isFocusVisible); } if (isPointerOver.HasValue) diff --git a/src/Avalonia.Input/Navigation/TabNavigation.cs b/src/Avalonia.Input/Navigation/TabNavigation.cs index dd50ea438a..cd377f1df6 100644 --- a/src/Avalonia.Input/Navigation/TabNavigation.cs +++ b/src/Avalonia.Input/Navigation/TabNavigation.cs @@ -219,7 +219,9 @@ namespace Avalonia.Input.Navigation if (parent != null) { - if (direction == NavigationDirection.Previous && parent.CanFocus()) + if (direction == NavigationDirection.Previous && + parent.CanFocus() && + KeyboardNavigation.GetIsTabStop((InputElement) parent)) { return parent; } diff --git a/src/Avalonia.Native/WindowImpl.cs b/src/Avalonia.Native/WindowImpl.cs index e91445000a..885591495b 100644 --- a/src/Avalonia.Native/WindowImpl.cs +++ b/src/Avalonia.Native/WindowImpl.cs @@ -1,6 +1,8 @@ using System; using Avalonia.Controls; using Avalonia.Controls.Platform; +using Avalonia.Input; +using Avalonia.Input.Raw; using Avalonia.Native.Interop; using Avalonia.OpenGL; using Avalonia.Platform; @@ -14,6 +16,8 @@ namespace Avalonia.Native private readonly AvaloniaNativePlatformOptions _opts; private readonly GlPlatformFeature _glFeature; IAvnWindow _native; + private double _extendTitleBarHeight = -1; + internal WindowImpl(IAvaloniaNativeFactory factory, AvaloniaNativePlatformOptions opts, GlPlatformFeature glFeature) : base(opts, glFeature) { @@ -50,6 +54,8 @@ namespace Avalonia.Native void IAvnWindowEvents.WindowStateChanged(AvnWindowState state) { + _parent.InvalidateExtendedMargins(); + _parent.WindowStateChanged?.Invoke((WindowState)state); } @@ -96,7 +102,85 @@ namespace Avalonia.Native } } - public Action WindowStateChanged { get; set; } + public Action WindowStateChanged { get; set; } + + public Action ExtendClientAreaToDecorationsChanged { get; set; } + + public Thickness ExtendedMargins { get; private set; } + + public Thickness OffScreenMargin { get; } = new Thickness(); + + private bool _isExtended; + public bool IsClientAreaExtendedToDecorations => _isExtended; + + protected override bool ChromeHitTest (RawPointerEventArgs e) + { + if(_isExtended) + { + if(e.Type == RawPointerEventType.LeftButtonDown) + { + var visual = (_inputRoot as Window).Renderer.HitTestFirst(e.Position, _inputRoot as Window, x => + { + if (x is IInputElement ie && !ie.IsHitTestVisible) + { + return false; + } + return true; + }); + + if(visual == null) + { + _native.BeginMoveDrag(); + } + } + } + + return false; + } + + private void InvalidateExtendedMargins() + { + if (WindowState == WindowState.FullScreen) + { + ExtendedMargins = new Thickness(); + } + else + { + ExtendedMargins = _isExtended ? new Thickness(0, _extendTitleBarHeight == -1 ? _native.GetExtendTitleBarHeight() : _extendTitleBarHeight, 0, 0) : new Thickness(); + } + + ExtendClientAreaToDecorationsChanged?.Invoke(_isExtended); + } + + /// + public void SetExtendClientAreaToDecorationsHint(bool extendIntoClientAreaHint) + { + _isExtended = extendIntoClientAreaHint; + + _native.SetExtendClientArea(extendIntoClientAreaHint); + + InvalidateExtendedMargins(); + } + + /// + public void SetExtendClientAreaChromeHints(ExtendClientAreaChromeHints hints) + { + _native.SetExtendClientAreaHints ((AvnExtendClientAreaChromeHints)hints); + } + + /// + public void SetExtendClientAreaTitleBarHeightHint(double titleBarHeight) + { + _extendTitleBarHeight = titleBarHeight; + _native.SetExtendTitleBarHeight(titleBarHeight); + + ExtendedMargins = _isExtended ? new Thickness(0, titleBarHeight == -1 ? _native.GetExtendTitleBarHeight() : titleBarHeight, 0, 0) : new Thickness(); + + ExtendClientAreaToDecorationsChanged?.Invoke(_isExtended); + } + + /// + public bool NeedsManagedDecorations => false; public void ShowTaskbarIcon(bool value) { diff --git a/src/Avalonia.Native/WindowImplBase.cs b/src/Avalonia.Native/WindowImplBase.cs index 9a90f65d1b..f19f9e97aa 100644 --- a/src/Avalonia.Native/WindowImplBase.cs +++ b/src/Avalonia.Native/WindowImplBase.cs @@ -46,7 +46,7 @@ namespace Avalonia.Native public abstract class WindowBaseImpl : IWindowBaseImpl, IFramebufferPlatformSurface, ITopLevelImplWithNativeControlHost { - IInputRoot _inputRoot; + protected IInputRoot _inputRoot; IAvnWindowBase _native; private object _syncRoot = new object(); private bool _deferredRendering = false; @@ -266,6 +266,11 @@ namespace Avalonia.Native return args.Handled; } + protected virtual bool ChromeHitTest(RawPointerEventArgs e) + { + return false; + } + public void RawMouseEvent(AvnRawMouseEventType type, uint timeStamp, AvnInputModifiers modifiers, AvnPoint point, AvnVector delta) { Dispatcher.UIThread.RunJobs(DispatcherPriority.Input + 1); @@ -277,7 +282,12 @@ namespace Avalonia.Native break; default: - Input?.Invoke(new RawPointerEventArgs(_mouse, timeStamp, _inputRoot, (RawPointerEventType)type, point.ToAvaloniaPoint(), (RawInputModifiers)modifiers)); + var e = new RawPointerEventArgs(_mouse, timeStamp, _inputRoot, (RawPointerEventType)type, point.ToAvaloniaPoint(), (RawInputModifiers)modifiers); + + if(!ChromeHitTest(e)) + { + Input?.Invoke(e); + } break; } } diff --git a/src/Avalonia.Styling/IStyledElement.cs b/src/Avalonia.Styling/IStyledElement.cs index 610c743a3e..37e6ed6fbb 100644 --- a/src/Avalonia.Styling/IStyledElement.cs +++ b/src/Avalonia.Styling/IStyledElement.cs @@ -17,11 +17,6 @@ namespace Avalonia /// event EventHandler Initialized; - /// - /// Raised when resources on the element are changed. - /// - event EventHandler ResourcesChanged; - /// /// Gets a value that indicates whether the element has finished initialization. /// diff --git a/src/Avalonia.Styling/StyledElement.cs b/src/Avalonia.Styling/StyledElement.cs index 05e031c9ec..65885ddebe 100644 --- a/src/Avalonia.Styling/StyledElement.cs +++ b/src/Avalonia.Styling/StyledElement.cs @@ -67,7 +67,6 @@ namespace Avalonia private List? _appliedStyles; private ITemplatedControl? _templatedParent; private bool _dataContextUpdating; - private bool _notifyingResourcesChanged; /// /// Initializes static members of the class. diff --git a/src/Avalonia.Styling/Styling/Styles.cs b/src/Avalonia.Styling/Styling/Styles.cs index 7c79060930..c752bdfeb8 100644 --- a/src/Avalonia.Styling/Styling/Styles.cs +++ b/src/Avalonia.Styling/Styling/Styles.cs @@ -21,7 +21,6 @@ namespace Avalonia.Styling private IResourceHost? _owner; private IResourceDictionary? _resources; private Dictionary?>? _cache; - private bool _notifyingResourcesChanged; public Styles() { diff --git a/src/Avalonia.Themes.Default/CaptionButtons.xaml b/src/Avalonia.Themes.Default/CaptionButtons.xaml new file mode 100644 index 0000000000..68249905a1 --- /dev/null +++ b/src/Avalonia.Themes.Default/CaptionButtons.xaml @@ -0,0 +1,69 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/Avalonia.Themes.Default/DefaultTheme.xaml b/src/Avalonia.Themes.Default/DefaultTheme.xaml index 94d26e798b..97cff5d94b 100644 --- a/src/Avalonia.Themes.Default/DefaultTheme.xaml +++ b/src/Avalonia.Themes.Default/DefaultTheme.xaml @@ -9,6 +9,7 @@ + diff --git a/src/Avalonia.Themes.Default/Slider.xaml b/src/Avalonia.Themes.Default/Slider.xaml index 1d48a946fc..ba4b1ee998 100644 --- a/src/Avalonia.Themes.Default/Slider.xaml +++ b/src/Avalonia.Themes.Default/Slider.xaml @@ -87,7 +87,10 @@ - + + diff --git a/src/Avalonia.Themes.Default/TitleBar.xaml b/src/Avalonia.Themes.Default/TitleBar.xaml new file mode 100644 index 0000000000..45798d3fa1 --- /dev/null +++ b/src/Avalonia.Themes.Default/TitleBar.xaml @@ -0,0 +1,53 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/src/Avalonia.Themes.Default/Window.xaml b/src/Avalonia.Themes.Default/Window.xaml index 0e7ec42856..739887fb35 100644 --- a/src/Avalonia.Themes.Default/Window.xaml +++ b/src/Avalonia.Themes.Default/Window.xaml @@ -5,7 +5,9 @@ - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/Avalonia.Themes.Fluent/FluentTheme.xaml b/src/Avalonia.Themes.Fluent/FluentTheme.xaml index 0cbbb77d38..2cb34431fa 100644 --- a/src/Avalonia.Themes.Fluent/FluentTheme.xaml +++ b/src/Avalonia.Themes.Fluent/FluentTheme.xaml @@ -7,7 +7,8 @@ - + + @@ -36,6 +37,7 @@ + diff --git a/src/Avalonia.Themes.Fluent/Slider.xaml b/src/Avalonia.Themes.Fluent/Slider.xaml index 539c448e0f..099c2000b8 100644 --- a/src/Avalonia.Themes.Fluent/Slider.xaml +++ b/src/Avalonia.Themes.Fluent/Slider.xaml @@ -182,6 +182,7 @@ diff --git a/src/Avalonia.Themes.Fluent/TitleBar.xaml b/src/Avalonia.Themes.Fluent/TitleBar.xaml new file mode 100644 index 0000000000..45798d3fa1 --- /dev/null +++ b/src/Avalonia.Themes.Fluent/TitleBar.xaml @@ -0,0 +1,53 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/src/Avalonia.Themes.Fluent/Window.xaml b/src/Avalonia.Themes.Fluent/Window.xaml index aee15347eb..d741e6c419 100644 --- a/src/Avalonia.Themes.Fluent/Window.xaml +++ b/src/Avalonia.Themes.Fluent/Window.xaml @@ -1,5 +1,5 @@ + + + + +"; + var loader = new AvaloniaXamlLoader(); + var window = (Window)loader.Load(xaml); + var foo = window.FindControl("foo"); + + Assert.Equal(Colors.Red, ((ISolidColorBrush)foo.Background).Color); + } + } + + [Fact] + public void Style_Can_Use_Pseudolass_Selector_With_Dash() + { + using (UnitTestApplication.Start(TestServices.StyledWindow)) + { + var xaml = @" + + + + + + + +"; + var loader = new AvaloniaXamlLoader(); + var window = (Window)loader.Load(xaml); + var foo = window.FindControl("foo"); + + Assert.Null(foo.Background); + + ((IPseudoClasses)foo.Classes).Add(":foo-bar"); + + Assert.Equal(Colors.Red, ((ISolidColorBrush)foo.Background).Color); + } + } } }