From 7b9d32af85aa08580380ce81d0a999db727a3240 Mon Sep 17 00:00:00 2001 From: Nathan Garside Date: Sun, 9 Feb 2020 15:50:42 +0000 Subject: [PATCH 01/24] Rework system decorations --- native/Avalonia.Native/inc/avalonia-native.h | 2 +- native/Avalonia.Native/src/OSX/window.mm | 41 +++++++++++++---- samples/ControlCatalog/MainView.xaml | 11 ++++- samples/ControlCatalog/MainView.xaml.cs | 14 ++++++ src/Avalonia.Controls/Platform/IWindowImpl.cs | 2 +- src/Avalonia.Controls/Window.cs | 44 ++++++++++++++++++- .../Remote/PreviewerWindowImpl.cs | 2 +- src/Avalonia.DesignerSupport/Remote/Stubs.cs | 2 +- src/Avalonia.Native/WindowImpl.cs | 4 +- src/Avalonia.X11/X11Window.cs | 16 ++++--- .../Interop/UnmanagedMethods.cs | 10 +++++ src/Windows/Avalonia.Win32/WindowImpl.cs | 20 +++++---- src/iOS/Avalonia.iOS/EmbeddableImpl.cs | 2 +- 13 files changed, 137 insertions(+), 33 deletions(-) diff --git a/native/Avalonia.Native/inc/avalonia-native.h b/native/Avalonia.Native/inc/avalonia-native.h index 4a960d47a1..ce4a592d67 100644 --- a/native/Avalonia.Native/inc/avalonia-native.h +++ b/native/Avalonia.Native/inc/avalonia-native.h @@ -236,7 +236,7 @@ AVNCOM(IAvnWindow, 04) : virtual IAvnWindowBase { virtual HRESULT ShowDialog (IAvnWindow* parent) = 0; virtual HRESULT SetCanResize(bool value) = 0; - virtual HRESULT SetHasDecorations(bool value) = 0; + virtual HRESULT SetHasDecorations(int value) = 0; virtual HRESULT SetTitle (void* utf8Title) = 0; virtual HRESULT SetTitleBarColor (AvnColor color) = 0; virtual HRESULT SetWindowState(AvnWindowState state) = 0; diff --git a/native/Avalonia.Native/src/OSX/window.mm b/native/Avalonia.Native/src/OSX/window.mm index b6ce172ffa..317d03162b 100644 --- a/native/Avalonia.Native/src/OSX/window.mm +++ b/native/Avalonia.Native/src/OSX/window.mm @@ -115,7 +115,6 @@ public: [NSApp activateIgnoringOtherApps:YES]; [Window setTitle:_lastTitle]; - [Window setTitleVisibility:NSWindowTitleVisible]; return S_OK; } @@ -411,7 +410,7 @@ class WindowImpl : public virtual WindowBaseImpl, public virtual IAvnWindow, pub { private: bool _canResize = true; - bool _hasDecorations = true; + int _hasDecorations = 2; CGRect _lastUndecoratedFrame; AvnWindowState _lastWindowState; @@ -476,12 +475,12 @@ private: bool IsZoomed () { - return _hasDecorations ? [Window isZoomed] : UndecoratedIsMaximized(); + return _hasDecorations > 0 ? [Window isZoomed] : UndecoratedIsMaximized(); } void DoZoom() { - if (_hasDecorations) + if (_hasDecorations > 0) { [Window performZoom:Window]; } @@ -506,13 +505,36 @@ private: } } - virtual HRESULT SetHasDecorations(bool value) override + virtual HRESULT SetHasDecorations(int value) override { @autoreleasepool { _hasDecorations = value; UpdateStyle(); + // full + if (_hasDecorations == 2) + { + [Window setHasShadow:YES]; + [Window setTitleVisibility:NSWindowTitleVisible]; + [Window setTitlebarAppearsTransparent:NO]; + [Window setTitle:_lastTitle]; + } + // border only + else if (_hasDecorations == 1) + { + [Window setHasShadow:YES]; + [Window setTitleVisibility:NSWindowTitleHidden]; + [Window setTitlebarAppearsTransparent:YES]; + } + // none + else + { + [Window setHasShadow:NO]; + [Window setTitleVisibility:NSWindowTitleHidden]; + [Window setTitlebarAppearsTransparent:YES]; + } + return S_OK; } } @@ -523,7 +545,6 @@ private: { _lastTitle = [NSString stringWithUTF8String:(const char*)utf8title]; [Window setTitle:_lastTitle]; - [Window setTitleVisibility:NSWindowTitleVisible]; return S_OK; } @@ -645,9 +666,11 @@ protected: virtual NSWindowStyleMask GetStyle() override { unsigned long s = NSWindowStyleMaskBorderless; - if(_hasDecorations) - s = s | NSWindowStyleMaskTitled | NSWindowStyleMaskClosable | NSWindowStyleMaskMiniaturizable; - if(_canResize) + if(_hasDecorations == 1) + s = s | NSWindowStyleMaskTitled | NSWindowStyleMaskFullSizeContentView; + if(_hasDecorations == 2) + s = s | NSWindowStyleMaskTitled | NSWindowStyleMaskClosable | NSWindowStyleMaskMiniaturizable | NSWindowStyleMaskBorderless; + if(_hasDecorations == 2 && _canResize) s = s | NSWindowStyleMaskResizable; return s; } diff --git a/samples/ControlCatalog/MainView.xaml b/samples/ControlCatalog/MainView.xaml index cbe2c62890..1c2653f73a 100644 --- a/samples/ControlCatalog/MainView.xaml +++ b/samples/ControlCatalog/MainView.xaml @@ -58,10 +58,17 @@ - + + + No Decorations + Border Only + Full Decorations + + Light Dark - + + diff --git a/samples/ControlCatalog/MainView.xaml.cs b/samples/ControlCatalog/MainView.xaml.cs index acb9bc5bc6..5f71b2ecd9 100644 --- a/samples/ControlCatalog/MainView.xaml.cs +++ b/samples/ControlCatalog/MainView.xaml.cs @@ -56,6 +56,20 @@ namespace ControlCatalog } }; Styles.Add(light); + + var decorations = this.Find("Decorations"); + decorations.SelectionChanged += (sender, e) => + { + Window window = (Window)VisualRoot; + window.SystemDecorations = (SystemDecorations)decorations.SelectedIndex; + }; + } + + protected override void OnAttachedToVisualTree(VisualTreeAttachmentEventArgs e) + { + base.OnAttachedToVisualTree(e); + var decorations = this.Find("Decorations"); + decorations.SelectedIndex = (int)((Window)VisualRoot).SystemDecorations; } } } diff --git a/src/Avalonia.Controls/Platform/IWindowImpl.cs b/src/Avalonia.Controls/Platform/IWindowImpl.cs index 91b895f38a..238070bbef 100644 --- a/src/Avalonia.Controls/Platform/IWindowImpl.cs +++ b/src/Avalonia.Controls/Platform/IWindowImpl.cs @@ -36,7 +36,7 @@ namespace Avalonia.Platform /// /// Enables or disables system window decorations (title bar, buttons, etc) /// - void SetSystemDecorations(bool enabled); + void SetSystemDecorations(SystemDecorations enabled); /// /// Sets the icon of this window. diff --git a/src/Avalonia.Controls/Window.cs b/src/Avalonia.Controls/Window.cs index f66a248aaf..853347bf41 100644 --- a/src/Avalonia.Controls/Window.cs +++ b/src/Avalonia.Controls/Window.cs @@ -45,6 +45,27 @@ namespace Avalonia.Controls WidthAndHeight = 3, } + /// + /// Determines system decorations (title bar, border, etc) for a + /// + public enum SystemDecorations + { + /// + /// No decorations + /// + None = 0, + + /// + /// Window border without titlebar + /// + BorderOnly = 1, + + /// + /// Fully decorated (default) + /// + Full = 2 + } + /// /// A top-level window. /// @@ -59,9 +80,16 @@ namespace Avalonia.Controls /// /// Enables or disables system window decorations (title bar, buttons, etc) /// + [Obsolete("Use SystemDecorationsProperty instead")] public static readonly StyledProperty HasSystemDecorationsProperty = AvaloniaProperty.Register(nameof(HasSystemDecorations), true); + /// + /// Defines the property. + /// + public static readonly StyledProperty SystemDecorationsProperty = + AvaloniaProperty.Register(nameof(SystemDecorations), SystemDecorations.Full); + /// /// Enables or disables the taskbar icon /// @@ -125,7 +153,9 @@ namespace Avalonia.Controls BackgroundProperty.OverrideDefaultValue(typeof(Window), Brushes.White); TitleProperty.Changed.AddClassHandler((s, e) => s.PlatformImpl?.SetTitle((string)e.NewValue)); HasSystemDecorationsProperty.Changed.AddClassHandler( - (s, e) => s.PlatformImpl?.SetSystemDecorations((bool)e.NewValue)); + (s, e) => s.PlatformImpl?.SetSystemDecorations(((bool)e.NewValue) ? SystemDecorations.Full : SystemDecorations.None)); + SystemDecorationsProperty.Changed.AddClassHandler( + (s, e) => s.PlatformImpl?.SetSystemDecorations((SystemDecorations)e.NewValue)); ShowInTaskbarProperty.Changed.AddClassHandler((w, e) => w.PlatformImpl?.ShowTaskbarIcon((bool)e.NewValue)); @@ -140,7 +170,6 @@ namespace Avalonia.Controls 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))); MaxHeightProperty.Changed.AddClassHandler((w, e) => w.PlatformImpl?.SetMinMaxSize(new Size(w.MinWidth, w.MinHeight), new Size(w.MaxWidth, (double)e.NewValue))); - } /// @@ -192,12 +221,23 @@ namespace Avalonia.Controls /// Enables or disables system window decorations (title bar, buttons, etc) /// /// + [Obsolete("Use SystemDecorations instead")] public bool HasSystemDecorations { get { return GetValue(HasSystemDecorationsProperty); } set { SetValue(HasSystemDecorationsProperty, value); } } + /// + /// Sets the system decorations (title bar, border, etc) + /// + /// + public SystemDecorations SystemDecorations + { + get { return GetValue(SystemDecorationsProperty); } + set { SetValue(SystemDecorationsProperty, value); } + } + /// /// Enables or disables the taskbar icon /// diff --git a/src/Avalonia.DesignerSupport/Remote/PreviewerWindowImpl.cs b/src/Avalonia.DesignerSupport/Remote/PreviewerWindowImpl.cs index 86e34ca6d4..7480b3519c 100644 --- a/src/Avalonia.DesignerSupport/Remote/PreviewerWindowImpl.cs +++ b/src/Avalonia.DesignerSupport/Remote/PreviewerWindowImpl.cs @@ -96,7 +96,7 @@ namespace Avalonia.DesignerSupport.Remote { } - public void SetSystemDecorations(bool enabled) + public void SetSystemDecorations(SystemDecorations enabled) { } diff --git a/src/Avalonia.DesignerSupport/Remote/Stubs.cs b/src/Avalonia.DesignerSupport/Remote/Stubs.cs index 4bba5ef41b..7bf1d236bd 100644 --- a/src/Avalonia.DesignerSupport/Remote/Stubs.cs +++ b/src/Avalonia.DesignerSupport/Remote/Stubs.cs @@ -110,7 +110,7 @@ namespace Avalonia.DesignerSupport.Remote { } - public void SetSystemDecorations(bool enabled) + public void SetSystemDecorations(SystemDecorations enabled) { } diff --git a/src/Avalonia.Native/WindowImpl.cs b/src/Avalonia.Native/WindowImpl.cs index c757576017..a540b026fa 100644 --- a/src/Avalonia.Native/WindowImpl.cs +++ b/src/Avalonia.Native/WindowImpl.cs @@ -68,9 +68,9 @@ namespace Avalonia.Native _native.CanResize = value; } - public void SetSystemDecorations(bool enabled) + public void SetSystemDecorations(SystemDecorations enabled) { - _native.HasDecorations = enabled; + _native.HasDecorations = (int)enabled; } public void SetTitleBarColor (Avalonia.Media.Color color) diff --git a/src/Avalonia.X11/X11Window.cs b/src/Avalonia.X11/X11Window.cs index 919abae243..b091ee212f 100644 --- a/src/Avalonia.X11/X11Window.cs +++ b/src/Avalonia.X11/X11Window.cs @@ -173,6 +173,7 @@ namespace Avalonia.X11 Surfaces = surfaces.ToArray(); UpdateMotifHints(); + UpdateSizeHints(null); _xic = XCreateIC(_x11.Xim, XNames.XNInputStyle, XIMProperties.XIMPreeditNothing | XIMProperties.XIMStatusNothing, XNames.XNClientWindow, _handle, IntPtr.Zero); XFlush(_x11.Display); @@ -219,12 +220,16 @@ namespace Avalonia.X11 var decorations = MotifDecorations.Menu | MotifDecorations.Title | MotifDecorations.Border | MotifDecorations.Maximize | MotifDecorations.Minimize | MotifDecorations.ResizeH; - if (_popup || !_systemDecorations) + if (_popup || _systemDecorations == SystemDecorations.None) { decorations = 0; } + else if (_systemDecorations == SystemDecorations.BorderOnly) + { + decorations = MotifDecorations.Border; + } - if (!_canResize) + if (!_canResize || _systemDecorations == SystemDecorations.BorderOnly) { functions &= ~(MotifFunctions.Resize | MotifFunctions.Maximize); decorations &= ~(MotifDecorations.Maximize | MotifDecorations.ResizeH); @@ -247,7 +252,7 @@ namespace Avalonia.X11 var min = _minMaxSize.minSize; var max = _minMaxSize.maxSize; - if (!_canResize) + if (!_canResize || _systemDecorations == SystemDecorations.BorderOnly) max = min = _realSize; if (preResize.HasValue) @@ -621,7 +626,7 @@ namespace Avalonia.X11 return rv; } - private bool _systemDecorations = true; + private SystemDecorations _systemDecorations = SystemDecorations.Full; private bool _canResize = true; private const int MaxWindowDimension = 100000; @@ -777,10 +782,11 @@ namespace Avalonia.X11 (int)(point.X * Scaling + Position.X), (int)(point.Y * Scaling + Position.Y)); - public void SetSystemDecorations(bool enabled) + public void SetSystemDecorations(SystemDecorations enabled) { _systemDecorations = enabled; UpdateMotifHints(); + UpdateSizeHints(null); } diff --git a/src/Windows/Avalonia.Win32/Interop/UnmanagedMethods.cs b/src/Windows/Avalonia.Win32/Interop/UnmanagedMethods.cs index 904e122382..50b568cab2 100644 --- a/src/Windows/Avalonia.Win32/Interop/UnmanagedMethods.cs +++ b/src/Windows/Avalonia.Win32/Interop/UnmanagedMethods.cs @@ -1298,7 +1298,17 @@ namespace Avalonia.Win32.Interop [DllImport("ole32.dll", CharSet = CharSet.Auto, ExactSpelling = true, PreserveSig = false)] internal static extern void DoDragDrop(IOleDataObject dataObject, IDropSource dropSource, int allowedEffects, out int finalEffect); + [DllImport("dwmapi.dll")] + public static extern int DwmExtendFrameIntoClientArea(IntPtr hwnd, ref MARGINS margins); + [StructLayout(LayoutKind.Sequential)] + internal struct MARGINS + { + public int cxLeftWidth; + public int cxRightWidth; + public int cyTopHeight; + public int cyBottomHeight; + } public enum MONITOR { diff --git a/src/Windows/Avalonia.Win32/WindowImpl.cs b/src/Windows/Avalonia.Win32/WindowImpl.cs index c16b76b539..d13c07279c 100644 --- a/src/Windows/Avalonia.Win32/WindowImpl.cs +++ b/src/Windows/Avalonia.Win32/WindowImpl.cs @@ -34,7 +34,7 @@ namespace Avalonia.Win32 private IInputRoot _owner; private ManagedDeferredRendererLock _rendererLock = new ManagedDeferredRendererLock(); private bool _trackingMouse; - private bool _decorated = true; + private SystemDecorations _decorated = SystemDecorations.Full; private bool _resizable = true; private bool _topmost = false; private bool _taskbarIcon = true; @@ -97,7 +97,7 @@ namespace Avalonia.Win32 { get { - if (_decorated) + if (_decorated == SystemDecorations.Full) { var style = UnmanagedMethods.GetWindowLong(_hwnd, (int)UnmanagedMethods.WindowLongParam.GWL_STYLE); var exStyle = UnmanagedMethods.GetWindowLong(_hwnd, (int)UnmanagedMethods.WindowLongParam.GWL_EXSTYLE); @@ -281,7 +281,7 @@ namespace Avalonia.Win32 UnmanagedMethods.ShowWindow(_hwnd, UnmanagedMethods.ShowWindowCommand.Hide); } - public void SetSystemDecorations(bool value) + public void SetSystemDecorations(SystemDecorations value) { if (value == _decorated) { @@ -464,7 +464,7 @@ namespace Avalonia.Win32 return IntPtr.Zero; case WindowsMessage.WM_NCCALCSIZE: - if (ToInt32(wParam) == 1 && !_decorated) + if (ToInt32(wParam) == 1 && _decorated != SystemDecorations.Full) { return IntPtr.Zero; } @@ -682,14 +682,14 @@ namespace Avalonia.Win32 break; case WindowsMessage.WM_NCPAINT: - if (!_decorated) + if (_decorated != SystemDecorations.Full) { return IntPtr.Zero; } break; case WindowsMessage.WM_NCACTIVATE: - if (!_decorated) + if (_decorated != SystemDecorations.Full) { return new IntPtr(1); } @@ -1001,7 +1001,7 @@ namespace Avalonia.Win32 style |= WindowStyles.WS_OVERLAPPEDWINDOW; - if (!_decorated) + if (_decorated != SystemDecorations.Full) { style ^= (WindowStyles.WS_CAPTION | WindowStyles.WS_SYSMENU); } @@ -1011,6 +1011,10 @@ namespace Avalonia.Win32 style ^= (WindowStyles.WS_SIZEFRAME); } + MARGINS margins = new MARGINS(); + margins.cyBottomHeight = _decorated == SystemDecorations.BorderOnly ? 1 : 0; + UnmanagedMethods.DwmExtendFrameIntoClientArea(_hwnd, ref margins); + GetClientRect(_hwnd, out var oldClientRect); var oldClientRectOrigin = new UnmanagedMethods.POINT(); ClientToScreen(_hwnd, ref oldClientRectOrigin); @@ -1024,7 +1028,7 @@ namespace Avalonia.Win32 if (oldDecorated != _decorated) { var newRect = oldClientRect; - if (_decorated) + if (_decorated == SystemDecorations.Full) AdjustWindowRectEx(ref newRect, (uint)style, false, GetWindowLong(_hwnd, (int)WindowLongParam.GWL_EXSTYLE)); SetWindowPos(_hwnd, IntPtr.Zero, newRect.left, newRect.top, newRect.Width, newRect.Height, diff --git a/src/iOS/Avalonia.iOS/EmbeddableImpl.cs b/src/iOS/Avalonia.iOS/EmbeddableImpl.cs index 65a6c15971..838bf49846 100644 --- a/src/iOS/Avalonia.iOS/EmbeddableImpl.cs +++ b/src/iOS/Avalonia.iOS/EmbeddableImpl.cs @@ -20,7 +20,7 @@ namespace Avalonia.iOS return Disposable.Empty; } - public void SetSystemDecorations(bool enabled) + public void SetSystemDecorations(SystemDecorations enabled) { } From d44ad423a0b2fec410a3917dcfb27d2187e9c8b4 Mon Sep 17 00:00:00 2001 From: Nathan Garside Date: Mon, 10 Feb 2020 09:08:33 +0000 Subject: [PATCH 02/24] Use enum in macOS native --- native/Avalonia.Native/src/OSX/window.h | 6 ++ native/Avalonia.Native/src/OSX/window.mm | 100 +++++++++++++---------- 2 files changed, 64 insertions(+), 42 deletions(-) diff --git a/native/Avalonia.Native/src/OSX/window.h b/native/Avalonia.Native/src/OSX/window.h index 3e626675d2..23e3c22db7 100644 --- a/native/Avalonia.Native/src/OSX/window.h +++ b/native/Avalonia.Native/src/OSX/window.h @@ -36,4 +36,10 @@ struct IWindowStateChanged virtual void WindowStateChanged () = 0; }; +typedef NS_ENUM(NSInteger, SystemDecorations) { + SystemDecorationsNone = 0, + SystemDecorationsBorderOnly = 1, + SystemDecorationsFull = 2, +}; + #endif /* window_h */ diff --git a/native/Avalonia.Native/src/OSX/window.mm b/native/Avalonia.Native/src/OSX/window.mm index 317d03162b..4c70f661b7 100644 --- a/native/Avalonia.Native/src/OSX/window.mm +++ b/native/Avalonia.Native/src/OSX/window.mm @@ -410,7 +410,7 @@ class WindowImpl : public virtual WindowBaseImpl, public virtual IAvnWindow, pub { private: bool _canResize = true; - int _hasDecorations = 2; + SystemDecorations _hasDecorations = SystemDecorationsFull; CGRect _lastUndecoratedFrame; AvnWindowState _lastWindowState; @@ -475,23 +475,26 @@ private: bool IsZoomed () { - return _hasDecorations > 0 ? [Window isZoomed] : UndecoratedIsMaximized(); + return _hasDecorations != SystemDecorationsNone ? [Window isZoomed] : UndecoratedIsMaximized(); } void DoZoom() { - if (_hasDecorations > 0) + switch (_hasDecorations) { - [Window performZoom:Window]; - } - else - { - if (!UndecoratedIsMaximized()) - { - _lastUndecoratedFrame = [Window frame]; - } - - [Window zoom:Window]; + case SystemDecorationsNone: + if (!UndecoratedIsMaximized()) + { + _lastUndecoratedFrame = [Window frame]; + } + + [Window zoom:Window]; + break; + + case SystemDecorationsBorderOnly: + case SystemDecorationsFull: + [Window performZoom:Window]; + break; } } @@ -509,32 +512,31 @@ private: { @autoreleasepool { - _hasDecorations = value; + _hasDecorations = (SystemDecorations)value; UpdateStyle(); - - // full - if (_hasDecorations == 2) - { - [Window setHasShadow:YES]; - [Window setTitleVisibility:NSWindowTitleVisible]; - [Window setTitlebarAppearsTransparent:NO]; - [Window setTitle:_lastTitle]; - } - // border only - else if (_hasDecorations == 1) - { - [Window setHasShadow:YES]; - [Window setTitleVisibility:NSWindowTitleHidden]; - [Window setTitlebarAppearsTransparent:YES]; - } - // none - else + + switch (_hasDecorations) { - [Window setHasShadow:NO]; - [Window setTitleVisibility:NSWindowTitleHidden]; - [Window setTitlebarAppearsTransparent:YES]; + case SystemDecorationsNone: + [Window setHasShadow:NO]; + [Window setTitleVisibility:NSWindowTitleHidden]; + [Window setTitlebarAppearsTransparent:YES]; + break; + + case SystemDecorationsBorderOnly: + [Window setHasShadow:YES]; + [Window setTitleVisibility:NSWindowTitleHidden]; + [Window setTitlebarAppearsTransparent:YES]; + break; + + case SystemDecorationsFull: + [Window setHasShadow:YES]; + [Window setTitleVisibility:NSWindowTitleVisible]; + [Window setTitlebarAppearsTransparent:NO]; + [Window setTitle:_lastTitle]; + break; } - + return S_OK; } } @@ -666,12 +668,26 @@ protected: virtual NSWindowStyleMask GetStyle() override { unsigned long s = NSWindowStyleMaskBorderless; - if(_hasDecorations == 1) - s = s | NSWindowStyleMaskTitled | NSWindowStyleMaskFullSizeContentView; - if(_hasDecorations == 2) - s = s | NSWindowStyleMaskTitled | NSWindowStyleMaskClosable | NSWindowStyleMaskMiniaturizable | NSWindowStyleMaskBorderless; - if(_hasDecorations == 2 && _canResize) - s = s | NSWindowStyleMaskResizable; + + switch (_hasDecorations) + { + case SystemDecorationsNone: + break; + + case SystemDecorationsBorderOnly: + s = s | NSWindowStyleMaskTitled | NSWindowStyleMaskFullSizeContentView; + break; + + case SystemDecorationsFull: + s = s | NSWindowStyleMaskTitled | NSWindowStyleMaskClosable | NSWindowStyleMaskMiniaturizable | NSWindowStyleMaskBorderless; + if(_canResize) + { + s = s | NSWindowStyleMaskResizable; + } + + break; + } + return s; } }; From c0e337d61f1df5a8b53135b1179c449b3f6544dc Mon Sep 17 00:00:00 2001 From: Nathan Garside Date: Mon, 10 Feb 2020 09:44:20 +0000 Subject: [PATCH 03/24] Use enum in mac interop --- native/Avalonia.Native/inc/avalonia-native.h | 8 +++++++- native/Avalonia.Native/src/OSX/window.h | 6 ------ native/Avalonia.Native/src/OSX/window.mm | 4 ++-- src/Avalonia.Native/WindowImpl.cs | 4 ++-- 4 files changed, 11 insertions(+), 11 deletions(-) diff --git a/native/Avalonia.Native/inc/avalonia-native.h b/native/Avalonia.Native/inc/avalonia-native.h index ce4a592d67..ee57f54e59 100644 --- a/native/Avalonia.Native/inc/avalonia-native.h +++ b/native/Avalonia.Native/inc/avalonia-native.h @@ -25,6 +25,12 @@ struct IAvnGlSurfaceRenderingSession; struct IAvnAppMenu; struct IAvnAppMenuItem; +enum SystemDecorations { + SystemDecorationsNone = 0, + SystemDecorationsBorderOnly = 1, + SystemDecorationsFull = 2, +}; + struct AvnSize { double Width, Height; @@ -236,7 +242,7 @@ AVNCOM(IAvnWindow, 04) : virtual IAvnWindowBase { virtual HRESULT ShowDialog (IAvnWindow* parent) = 0; virtual HRESULT SetCanResize(bool value) = 0; - virtual HRESULT SetHasDecorations(int value) = 0; + virtual HRESULT SetHasDecorations(SystemDecorations value) = 0; virtual HRESULT SetTitle (void* utf8Title) = 0; virtual HRESULT SetTitleBarColor (AvnColor color) = 0; virtual HRESULT SetWindowState(AvnWindowState state) = 0; diff --git a/native/Avalonia.Native/src/OSX/window.h b/native/Avalonia.Native/src/OSX/window.h index 23e3c22db7..3e626675d2 100644 --- a/native/Avalonia.Native/src/OSX/window.h +++ b/native/Avalonia.Native/src/OSX/window.h @@ -36,10 +36,4 @@ struct IWindowStateChanged virtual void WindowStateChanged () = 0; }; -typedef NS_ENUM(NSInteger, SystemDecorations) { - SystemDecorationsNone = 0, - SystemDecorationsBorderOnly = 1, - SystemDecorationsFull = 2, -}; - #endif /* window_h */ diff --git a/native/Avalonia.Native/src/OSX/window.mm b/native/Avalonia.Native/src/OSX/window.mm index 4c70f661b7..2c03407732 100644 --- a/native/Avalonia.Native/src/OSX/window.mm +++ b/native/Avalonia.Native/src/OSX/window.mm @@ -508,11 +508,11 @@ private: } } - virtual HRESULT SetHasDecorations(int value) override + virtual HRESULT SetHasDecorations(SystemDecorations value) override { @autoreleasepool { - _hasDecorations = (SystemDecorations)value; + _hasDecorations = value; UpdateStyle(); switch (_hasDecorations) diff --git a/src/Avalonia.Native/WindowImpl.cs b/src/Avalonia.Native/WindowImpl.cs index a540b026fa..73ec81ce57 100644 --- a/src/Avalonia.Native/WindowImpl.cs +++ b/src/Avalonia.Native/WindowImpl.cs @@ -68,9 +68,9 @@ namespace Avalonia.Native _native.CanResize = value; } - public void SetSystemDecorations(SystemDecorations enabled) + public void SetSystemDecorations(Controls.SystemDecorations enabled) { - _native.HasDecorations = (int)enabled; + _native.HasDecorations = (Interop.SystemDecorations)enabled; } public void SetTitleBarColor (Avalonia.Media.Color color) From c34bfc56f8d3baa8b7f3e6d116e27edaa9a46a3a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=92=D0=B0=D0=B4=D0=B8=D0=BC=20=D0=9C=D0=B5=D0=BB=D1=8C?= =?UTF-8?q?=D0=BD=D0=B8=D0=BA=D0=BE=D0=B2?= Date: Tue, 18 Feb 2020 16:25:28 +0300 Subject: [PATCH 04/24] Add class CroppedBitmap --- samples/ControlCatalog/Pages/ImagePage.xaml | 15 +++++- .../ControlCatalog/Pages/ImagePage.xaml.cs | 41 ++++++++++++++++ .../Media/Imaging/CroppedBitmap.cs | 47 +++++++++++++++++++ 3 files changed, 102 insertions(+), 1 deletion(-) create mode 100644 src/Avalonia.Visuals/Media/Imaging/CroppedBitmap.cs diff --git a/samples/ControlCatalog/Pages/ImagePage.xaml b/samples/ControlCatalog/Pages/ImagePage.xaml index 9b8f8af765..c20f76cedd 100644 --- a/samples/ControlCatalog/Pages/ImagePage.xaml +++ b/samples/ControlCatalog/Pages/ImagePage.xaml @@ -7,7 +7,7 @@ Displays an image - + Bitmap @@ -22,6 +22,19 @@ + Crop + + None + Center + TopLeft + TopRight + BottomLeft + BottomRight + + + + + Drawing None diff --git a/samples/ControlCatalog/Pages/ImagePage.xaml.cs b/samples/ControlCatalog/Pages/ImagePage.xaml.cs index bbe89d1dfd..d637c88102 100644 --- a/samples/ControlCatalog/Pages/ImagePage.xaml.cs +++ b/samples/ControlCatalog/Pages/ImagePage.xaml.cs @@ -1,6 +1,10 @@ +using System; +using Avalonia; using Avalonia.Controls; using Avalonia.Markup.Xaml; using Avalonia.Media; +using Avalonia.Media.Imaging; +using Avalonia.Platform; namespace ControlCatalog.Pages { @@ -8,12 +12,17 @@ namespace ControlCatalog.Pages { private readonly Image _bitmapImage; private readonly Image _drawingImage; + private readonly Image _croppedImage; + private readonly IBitmap _croppedBitmapSource; public ImagePage() { InitializeComponent(); _bitmapImage = this.FindControl("bitmapImage"); _drawingImage = this.FindControl("drawingImage"); + _croppedImage = this.FindControl("croppedImage"); + _croppedBitmapSource = LoadBitmap("avares://ControlCatalog/Assets/delicate-arch-896885_640.jpg"); + _croppedImage.Source = new CroppedBitmap(_croppedBitmapSource, default); } private void InitializeComponent() @@ -38,5 +47,37 @@ namespace ControlCatalog.Pages _drawingImage.Stretch = (Stretch)comboxBox.SelectedIndex; } } + + public void BitmapCropChanged(object sender, SelectionChangedEventArgs e) + { + if (_croppedImage != null) + { + var comboxBox = (ComboBox)sender; + _croppedImage.Source = new CroppedBitmap( _croppedBitmapSource, GetCropRect(comboxBox.SelectedIndex)); + } + } + + private PixelRect GetCropRect(int index) + { + var bitmapWidth = _croppedBitmapSource.PixelSize.Width; + var bitmapHeight = _croppedBitmapSource.PixelSize.Height; + var cropSize = new PixelSize(bitmapWidth / 2, bitmapHeight / 2); + return index switch + { + 1 => new PixelRect(new PixelPoint((bitmapWidth - cropSize.Width) / 2, (bitmapHeight - cropSize.Width) / 2), cropSize), + 2 => new PixelRect(new PixelPoint(0, 0), cropSize), + 3 => new PixelRect(new PixelPoint(bitmapWidth - cropSize.Width, 0), cropSize), + 4 => new PixelRect(new PixelPoint(0, bitmapHeight - cropSize.Height), cropSize), + 5 => new PixelRect(new PixelPoint(bitmapWidth - cropSize.Width, bitmapHeight - cropSize.Height), cropSize), + _ => PixelRect.Empty + }; + + } + + private IBitmap LoadBitmap(string uri) + { + var assets = AvaloniaLocator.Current.GetService(); + return new Bitmap(assets.Open(new Uri(uri))); + } } } diff --git a/src/Avalonia.Visuals/Media/Imaging/CroppedBitmap.cs b/src/Avalonia.Visuals/Media/Imaging/CroppedBitmap.cs new file mode 100644 index 0000000000..6bdee24c03 --- /dev/null +++ b/src/Avalonia.Visuals/Media/Imaging/CroppedBitmap.cs @@ -0,0 +1,47 @@ +using System; +using System.Collections.Generic; +using System.Text; +using Avalonia.Visuals.Media.Imaging; + +namespace Avalonia.Media.Imaging +{ + public class CroppedBitmap : IImage, IDisposable + { + public CroppedBitmap() + { + Source = null; + SourceRect = default; + } + public CroppedBitmap(IBitmap source, PixelRect sourceRect) + { + Source = source; + SourceRect = sourceRect; + } + public virtual void Dispose() + { + Source?.Dispose(); + } + + public Size Size { + get + { + if (Source == null) + return Size.Empty; + if (SourceRect.IsEmpty) + return Source.Size; + return SourceRect.Size.ToSizeWithDpi(Source.Dpi); + } + } + + public void Draw(DrawingContext context, Rect sourceRect, Rect destRect, BitmapInterpolationMode bitmapInterpolationMode) + { + if (Source == null) + return; + var topLeft = SourceRect.TopLeft.ToPointWithDpi(Source.Dpi); + Source.Draw(context, sourceRect.Translate(new Vector(topLeft.X, topLeft.Y)), destRect, bitmapInterpolationMode); + } + + public IBitmap Source { get; } + public PixelRect SourceRect { get; } + } +} From 3aab2d5619a09835e48d3abe8106f8f543743cd4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=92=D0=B0=D0=B4=D0=B8=D0=BC=20=D0=9C=D0=B5=D0=BB=D1=8C?= =?UTF-8?q?=D0=BD=D0=B8=D0=BA=D0=BE=D0=B2?= Date: Mon, 2 Mar 2020 10:40:41 +0300 Subject: [PATCH 05/24] Added functionality to use CroppedBitmap directly in XAML --- samples/ControlCatalog/Pages/ImagePage.xaml | 8 ++- .../ControlCatalog/Pages/ImagePage.xaml.cs | 18 ++--- .../Media/Imaging/CroppedBitmap.cs | 69 ++++++++++++++++--- .../Properties/AssemblyInfo.cs | 3 +- 4 files changed, 72 insertions(+), 26 deletions(-) diff --git a/samples/ControlCatalog/Pages/ImagePage.xaml b/samples/ControlCatalog/Pages/ImagePage.xaml index c20f76cedd..f61931ed72 100644 --- a/samples/ControlCatalog/Pages/ImagePage.xaml +++ b/samples/ControlCatalog/Pages/ImagePage.xaml @@ -23,7 +23,7 @@ Crop - + None Center TopLeft @@ -31,7 +31,11 @@ BottomLeft BottomRight - + + + + + diff --git a/samples/ControlCatalog/Pages/ImagePage.xaml.cs b/samples/ControlCatalog/Pages/ImagePage.xaml.cs index d637c88102..d8f4d6d5a2 100644 --- a/samples/ControlCatalog/Pages/ImagePage.xaml.cs +++ b/samples/ControlCatalog/Pages/ImagePage.xaml.cs @@ -13,7 +13,6 @@ namespace ControlCatalog.Pages private readonly Image _bitmapImage; private readonly Image _drawingImage; private readonly Image _croppedImage; - private readonly IBitmap _croppedBitmapSource; public ImagePage() { @@ -21,8 +20,6 @@ namespace ControlCatalog.Pages _bitmapImage = this.FindControl("bitmapImage"); _drawingImage = this.FindControl("drawingImage"); _croppedImage = this.FindControl("croppedImage"); - _croppedBitmapSource = LoadBitmap("avares://ControlCatalog/Assets/delicate-arch-896885_640.jpg"); - _croppedImage.Source = new CroppedBitmap(_croppedBitmapSource, default); } private void InitializeComponent() @@ -53,15 +50,16 @@ namespace ControlCatalog.Pages if (_croppedImage != null) { var comboxBox = (ComboBox)sender; - _croppedImage.Source = new CroppedBitmap( _croppedBitmapSource, GetCropRect(comboxBox.SelectedIndex)); + var croppedBitmap = _croppedImage.Source as CroppedBitmap; + croppedBitmap.SourceRect = GetCropRect(comboxBox.SelectedIndex); } } private PixelRect GetCropRect(int index) { - var bitmapWidth = _croppedBitmapSource.PixelSize.Width; - var bitmapHeight = _croppedBitmapSource.PixelSize.Height; - var cropSize = new PixelSize(bitmapWidth / 2, bitmapHeight / 2); + var bitmapWidth = 640; + var bitmapHeight = 426; + var cropSize = new PixelSize(320, 240); return index switch { 1 => new PixelRect(new PixelPoint((bitmapWidth - cropSize.Width) / 2, (bitmapHeight - cropSize.Width) / 2), cropSize), @@ -73,11 +71,5 @@ namespace ControlCatalog.Pages }; } - - private IBitmap LoadBitmap(string uri) - { - var assets = AvaloniaLocator.Current.GetService(); - return new Bitmap(assets.Open(new Uri(uri))); - } } } diff --git a/src/Avalonia.Visuals/Media/Imaging/CroppedBitmap.cs b/src/Avalonia.Visuals/Media/Imaging/CroppedBitmap.cs index 6bdee24c03..70823e114c 100644 --- a/src/Avalonia.Visuals/Media/Imaging/CroppedBitmap.cs +++ b/src/Avalonia.Visuals/Media/Imaging/CroppedBitmap.cs @@ -1,25 +1,77 @@ using System; -using System.Collections.Generic; -using System.Text; using Avalonia.Visuals.Media.Imaging; namespace Avalonia.Media.Imaging { - public class CroppedBitmap : IImage, IDisposable + /// + /// Crops a Bitmap. + /// + public class CroppedBitmap : AvaloniaObject, IImage, IAffectsRender, IDisposable { + /// + /// Defines the property. + /// + public static readonly StyledProperty SourceProperty = + AvaloniaProperty.Register(nameof(Source)); + + /// + /// Defines the property. + /// + public static readonly StyledProperty SourceRectProperty = + AvaloniaProperty.Register(nameof(SourceRect)); + + public event EventHandler Invalidated; + + static CroppedBitmap() + { + SourceRectProperty.Changed.AddClassHandler((x, e) => x.SourceRectChanged(e)); + SourceProperty.Changed.AddClassHandler((x, e) => x.SourceChanged(e)); + } + + /// + /// Gets or sets the source for the bitmap. + /// + public IImage Source + { + get => GetValue(SourceProperty); + set => SetValue(SourceProperty, value); + } + + /// + /// Gets or sets the rectangular area that the bitmap is cropped to. + /// + public PixelRect SourceRect + { + get => GetValue(SourceRectProperty); + set => SetValue(SourceRectProperty, value); + } + public CroppedBitmap() { Source = null; SourceRect = default; } - public CroppedBitmap(IBitmap source, PixelRect sourceRect) + + public CroppedBitmap(IImage source, PixelRect sourceRect) { Source = source; SourceRect = sourceRect; } + + private void SourceChanged(AvaloniaPropertyChangedEventArgs e) + { + if (e.NewValue == null) + return; + if (!(e.NewValue is IBitmap)) + throw new ArgumentException("Only IBitmap supported as source"); + Invalidated?.Invoke(this, e); + } + + private void SourceRectChanged(AvaloniaPropertyChangedEventArgs e) => Invalidated?.Invoke(this, e); + public virtual void Dispose() { - Source?.Dispose(); + (Source as IBitmap)?.Dispose(); } public Size Size { @@ -29,7 +81,7 @@ namespace Avalonia.Media.Imaging return Size.Empty; if (SourceRect.IsEmpty) return Source.Size; - return SourceRect.Size.ToSizeWithDpi(Source.Dpi); + return SourceRect.Size.ToSizeWithDpi((Source as IBitmap).Dpi); } } @@ -37,11 +89,8 @@ namespace Avalonia.Media.Imaging { if (Source == null) return; - var topLeft = SourceRect.TopLeft.ToPointWithDpi(Source.Dpi); + var topLeft = SourceRect.TopLeft.ToPointWithDpi((Source as IBitmap).Dpi); Source.Draw(context, sourceRect.Translate(new Vector(topLeft.X, topLeft.Y)), destRect, bitmapInterpolationMode); } - - public IBitmap Source { get; } - public PixelRect SourceRect { get; } } } diff --git a/src/Avalonia.Visuals/Properties/AssemblyInfo.cs b/src/Avalonia.Visuals/Properties/AssemblyInfo.cs index 05c3d7e62a..8f08bc5f44 100644 --- a/src/Avalonia.Visuals/Properties/AssemblyInfo.cs +++ b/src/Avalonia.Visuals/Properties/AssemblyInfo.cs @@ -8,7 +8,8 @@ using Avalonia.Metadata; [assembly: InternalsVisibleTo("Avalonia.Visuals.UnitTests")] [assembly: XmlnsDefinition("https://github.com/avaloniaui", "Avalonia.Animation")] [assembly: XmlnsDefinition("https://github.com/avaloniaui", "Avalonia.Media")] +[assembly: XmlnsDefinition("https://github.com/avaloniaui", "Avalonia.Media.Imaging")] [assembly: XmlnsDefinition("https://github.com/avaloniaui", "Avalonia")] [assembly: InternalsVisibleTo("Avalonia.Direct2D1.RenderTests")] -[assembly: InternalsVisibleTo("Avalonia.Skia.RenderTests")] \ No newline at end of file +[assembly: InternalsVisibleTo("Avalonia.Skia.RenderTests")] From e5f0ac3dda380bf7a7058ed31df00a7e2383db3a Mon Sep 17 00:00:00 2001 From: Nathan Garside Date: Tue, 3 Mar 2020 22:23:13 +0000 Subject: [PATCH 06/24] Fix event binding for HasSystemDecorations property --- src/Avalonia.Controls/Window.cs | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/Avalonia.Controls/Window.cs b/src/Avalonia.Controls/Window.cs index 853347bf41..b2bd52e8a2 100644 --- a/src/Avalonia.Controls/Window.cs +++ b/src/Avalonia.Controls/Window.cs @@ -153,9 +153,13 @@ namespace Avalonia.Controls BackgroundProperty.OverrideDefaultValue(typeof(Window), Brushes.White); TitleProperty.Changed.AddClassHandler((s, e) => s.PlatformImpl?.SetTitle((string)e.NewValue)); HasSystemDecorationsProperty.Changed.AddClassHandler( - (s, e) => s.PlatformImpl?.SetSystemDecorations(((bool)e.NewValue) ? SystemDecorations.Full : SystemDecorations.None)); + (s, e) => s.SetValue(SystemDecorationsProperty, (bool)e.NewValue ? SystemDecorations.Full : SystemDecorations.None)); SystemDecorationsProperty.Changed.AddClassHandler( - (s, e) => s.PlatformImpl?.SetSystemDecorations((SystemDecorations)e.NewValue)); + (s, e) => + { + s.PlatformImpl?.SetSystemDecorations((SystemDecorations)e.NewValue); + s.SetValue(HasSystemDecorationsProperty, (SystemDecorations)e.NewValue != SystemDecorations.None); + }); ShowInTaskbarProperty.Changed.AddClassHandler((w, e) => w.PlatformImpl?.ShowTaskbarIcon((bool)e.NewValue)); From 0ee508bfa84d2695b08d46f52562192fcfcc4e1e Mon Sep 17 00:00:00 2001 From: Dariusz Komosinski Date: Mon, 9 Mar 2020 23:09:35 +0100 Subject: [PATCH 07/24] Fix CalendarBenchmark not working with new text layout. --- tests/Avalonia.Benchmarks/NullGlyphRun.cs | 11 +++++++++++ tests/Avalonia.Benchmarks/NullRenderingPlatform.cs | 4 +++- 2 files changed, 14 insertions(+), 1 deletion(-) create mode 100644 tests/Avalonia.Benchmarks/NullGlyphRun.cs diff --git a/tests/Avalonia.Benchmarks/NullGlyphRun.cs b/tests/Avalonia.Benchmarks/NullGlyphRun.cs new file mode 100644 index 0000000000..5ba0822649 --- /dev/null +++ b/tests/Avalonia.Benchmarks/NullGlyphRun.cs @@ -0,0 +1,11 @@ +using Avalonia.Platform; + +namespace Avalonia.Benchmarks +{ + internal class NullGlyphRun : IGlyphRunImpl + { + public void Dispose() + { + } + } +} diff --git a/tests/Avalonia.Benchmarks/NullRenderingPlatform.cs b/tests/Avalonia.Benchmarks/NullRenderingPlatform.cs index 101d40f00a..1f983069c2 100644 --- a/tests/Avalonia.Benchmarks/NullRenderingPlatform.cs +++ b/tests/Avalonia.Benchmarks/NullRenderingPlatform.cs @@ -72,7 +72,9 @@ namespace Avalonia.Benchmarks public IGlyphRunImpl CreateGlyphRun(GlyphRun glyphRun, out double width) { - throw new NotImplementedException(); + width = default; + + return new NullGlyphRun(); } } } From 35bce7b031142e9932c86f07595a322f02447696 Mon Sep 17 00:00:00 2001 From: Dariusz Komosinski Date: Mon, 9 Mar 2020 23:11:47 +0100 Subject: [PATCH 08/24] Avoid creating handler unsubscribe disposables unless explicitly requested. --- src/Avalonia.Controls/Calendar/DatePicker.cs | 2 +- src/Avalonia.Controls/ComboBox.cs | 3 +- src/Avalonia.Controls/Primitives/Popup.cs | 2 +- .../Diagnostics/DevTools.cs | 2 +- src/Avalonia.Interactivity/IInteractive.cs | 4 +-- src/Avalonia.Interactivity/Interactive.cs | 32 ++++--------------- .../InteractiveExtensions.cs | 25 ++++++++++++++- 7 files changed, 37 insertions(+), 33 deletions(-) diff --git a/src/Avalonia.Controls/Calendar/DatePicker.cs b/src/Avalonia.Controls/Calendar/DatePicker.cs index ea06bdb394..07e42c64e4 100644 --- a/src/Avalonia.Controls/Calendar/DatePicker.cs +++ b/src/Avalonia.Controls/Calendar/DatePicker.cs @@ -476,7 +476,7 @@ namespace Avalonia.Controls { _dropDownButton.Click += DropDownButton_Click; _buttonPointerPressedSubscription = - _dropDownButton.AddHandler(PointerPressedEvent, DropDownButton_PointerPressed, handledEventsToo: true); + _dropDownButton.AddDisposableHandler(PointerPressedEvent, DropDownButton_PointerPressed, handledEventsToo: true); } if (_textBox != null) diff --git a/src/Avalonia.Controls/ComboBox.cs b/src/Avalonia.Controls/ComboBox.cs index 9d471a0fc0..d1f54da23a 100644 --- a/src/Avalonia.Controls/ComboBox.cs +++ b/src/Avalonia.Controls/ComboBox.cs @@ -9,6 +9,7 @@ using Avalonia.Controls.Primitives; using Avalonia.Controls.Shapes; using Avalonia.Controls.Templates; using Avalonia.Input; +using Avalonia.Interactivity; using Avalonia.LogicalTree; using Avalonia.Media; using Avalonia.VisualTree; @@ -265,7 +266,7 @@ namespace Avalonia.Controls var toplevel = this.GetVisualRoot() as TopLevel; if (toplevel != null) { - _subscriptionsOnOpen = toplevel.AddHandler(PointerWheelChangedEvent, (s, ev) => + _subscriptionsOnOpen = toplevel.AddDisposableHandler(PointerWheelChangedEvent, (s, ev) => { //eat wheel scroll event outside dropdown popup while it's open if (IsDropDownOpen && (ev.Source as IVisual).GetVisualRoot() == toplevel) diff --git a/src/Avalonia.Controls/Primitives/Popup.cs b/src/Avalonia.Controls/Primitives/Popup.cs index 9dd481cf40..1a12b53b9d 100644 --- a/src/Avalonia.Controls/Primitives/Popup.cs +++ b/src/Avalonia.Controls/Primitives/Popup.cs @@ -279,7 +279,7 @@ namespace Avalonia.Controls.Primitives } } - DeferCleanup(topLevel.AddHandler(PointerPressedEvent, PointerPressedOutside, RoutingStrategies.Tunnel)); + DeferCleanup(topLevel.AddDisposableHandler(PointerPressedEvent, PointerPressedOutside, RoutingStrategies.Tunnel)); DeferCleanup(InputManager.Instance?.Process.Subscribe(ListenForNonClientClick)); diff --git a/src/Avalonia.Diagnostics/Diagnostics/DevTools.cs b/src/Avalonia.Diagnostics/Diagnostics/DevTools.cs index 0464047273..b3a8f4745e 100644 --- a/src/Avalonia.Diagnostics/Diagnostics/DevTools.cs +++ b/src/Avalonia.Diagnostics/Diagnostics/DevTools.cs @@ -22,7 +22,7 @@ namespace Avalonia.Diagnostics } } - return root.AddHandler( + return root.AddDisposableHandler( InputElement.KeyDownEvent, PreviewKeyDown, RoutingStrategies.Tunnel); diff --git a/src/Avalonia.Interactivity/IInteractive.cs b/src/Avalonia.Interactivity/IInteractive.cs index 33baa9453a..51aee78988 100644 --- a/src/Avalonia.Interactivity/IInteractive.cs +++ b/src/Avalonia.Interactivity/IInteractive.cs @@ -23,7 +23,7 @@ namespace Avalonia.Interactivity /// The routing strategies to listen to. /// Whether handled events should also be listened for. /// A disposable that terminates the event subscription. - IDisposable AddHandler( + void AddHandler( RoutedEvent routedEvent, Delegate handler, RoutingStrategies routes = RoutingStrategies.Direct | RoutingStrategies.Bubble, @@ -38,7 +38,7 @@ namespace Avalonia.Interactivity /// The routing strategies to listen to. /// Whether handled events should also be listened for. /// A disposable that terminates the event subscription. - IDisposable AddHandler( + void AddHandler( RoutedEvent routedEvent, EventHandler handler, RoutingStrategies routes = RoutingStrategies.Direct | RoutingStrategies.Bubble, diff --git a/src/Avalonia.Interactivity/Interactive.cs b/src/Avalonia.Interactivity/Interactive.cs index 9493d86885..321ecbc516 100644 --- a/src/Avalonia.Interactivity/Interactive.cs +++ b/src/Avalonia.Interactivity/Interactive.cs @@ -27,8 +27,7 @@ namespace Avalonia.Interactivity /// The handler. /// The routing strategies to listen to. /// Whether handled events should also be listened for. - /// A disposable that terminates the event subscription. - public IDisposable AddHandler( + public void AddHandler( RoutedEvent routedEvent, Delegate handler, RoutingStrategies routes = RoutingStrategies.Direct | RoutingStrategies.Bubble, @@ -38,7 +37,8 @@ namespace Avalonia.Interactivity handler = handler ?? throw new ArgumentNullException(nameof(handler)); var subscription = new EventSubscription(handler, routes, handledEventsToo); - return AddEventSubscription(routedEvent, subscription); + + AddEventSubscription(routedEvent, subscription); } /// @@ -49,8 +49,7 @@ namespace Avalonia.Interactivity /// The handler. /// The routing strategies to listen to. /// Whether handled events should also be listened for. - /// A disposable that terminates the event subscription. - public IDisposable AddHandler( + public void AddHandler( RoutedEvent routedEvent, EventHandler handler, RoutingStrategies routes = RoutingStrategies.Direct | RoutingStrategies.Bubble, @@ -69,7 +68,7 @@ namespace Avalonia.Interactivity var subscription = new EventSubscription(handler, routes, handledEventsToo, (baseHandler, sender, args) => InvokeAdapter(baseHandler, sender, args)); - return AddEventSubscription(routedEvent, subscription); + AddEventSubscription(routedEvent, subscription); } /// @@ -188,7 +187,7 @@ namespace Avalonia.Interactivity return result; } - private IDisposable AddEventSubscription(RoutedEvent routedEvent, EventSubscription subscription) + private void AddEventSubscription(RoutedEvent routedEvent, EventSubscription subscription) { _eventHandlers ??= new Dictionary>(); @@ -199,8 +198,6 @@ namespace Avalonia.Interactivity } subscriptions.Add(subscription); - - return new UnsubscribeDisposable(subscriptions, subscription); } private readonly struct EventSubscription @@ -225,22 +222,5 @@ namespace Avalonia.Interactivity public bool HandledEventsToo { get; } } - - private sealed class UnsubscribeDisposable : IDisposable - { - private readonly List _subscriptions; - private readonly EventSubscription _subscription; - - public UnsubscribeDisposable(List subscriptions, EventSubscription subscription) - { - _subscriptions = subscriptions; - _subscription = subscription; - } - - public void Dispose() - { - _subscriptions.Remove(_subscription); - } - } } } diff --git a/src/Avalonia.Interactivity/InteractiveExtensions.cs b/src/Avalonia.Interactivity/InteractiveExtensions.cs index 414c408080..e6c93e26b2 100644 --- a/src/Avalonia.Interactivity/InteractiveExtensions.cs +++ b/src/Avalonia.Interactivity/InteractiveExtensions.cs @@ -2,6 +2,7 @@ // Licensed under the MIT license. See licence.md file in the project root for full license information. using System; +using System.Reactive.Disposables; using System.Reactive.Linq; namespace Avalonia.Interactivity @@ -11,6 +12,28 @@ namespace Avalonia.Interactivity /// public static class InteractiveExtensions { + /// + /// Adds a handler for the specified routed event and returns a disposable that can terminate the event subscription. + /// + /// The type of the event's args. + /// Target for adding given event handler. + /// The routed event. + /// The handler. + /// The routing strategies to listen to. + /// Whether handled events should also be listened for. + /// A disposable that terminates the event subscription. + public static IDisposable AddDisposableHandler(this IInteractive o, RoutedEvent routedEvent, + EventHandler handler, + RoutingStrategies routes = RoutingStrategies.Direct | RoutingStrategies.Bubble, + bool handledEventsToo = false) where TEventArgs : RoutedEventArgs + { + o.AddHandler(routedEvent, handler, routes, handledEventsToo); + + return Disposable.Create( + (instance: o, handler, routedEvent), + state => state.instance.RemoveHandler(state.routedEvent, state.handler)); + } + /// /// Gets an observable for a . /// @@ -31,7 +54,7 @@ namespace Avalonia.Interactivity o = o ?? throw new ArgumentNullException(nameof(o)); routedEvent = routedEvent ?? throw new ArgumentNullException(nameof(routedEvent)); - return Observable.Create(x => o.AddHandler( + return Observable.Create(x => o.AddDisposableHandler( routedEvent, (_, e) => x.OnNext(e), routes, From 2d78458f7484d8a5f0b67aa5d30f4bfec784184b Mon Sep 17 00:00:00 2001 From: Benedikt Schroeder Date: Tue, 10 Mar 2020 07:52:01 +0100 Subject: [PATCH 09/24] Add padding --- src/Avalonia.Controls/TextBlock.cs | 35 +++++++++++++++++++++++------- 1 file changed, 27 insertions(+), 8 deletions(-) diff --git a/src/Avalonia.Controls/TextBlock.cs b/src/Avalonia.Controls/TextBlock.cs index 1655e22331..560c1952ac 100644 --- a/src/Avalonia.Controls/TextBlock.cs +++ b/src/Avalonia.Controls/TextBlock.cs @@ -2,6 +2,7 @@ // Licensed under the MIT license. See licence.md file in the project root for full license information. using System.Reactive.Linq; +using Avalonia.Layout; using Avalonia.LogicalTree; using Avalonia.Media; using Avalonia.Media.TextFormatting; @@ -20,6 +21,12 @@ namespace Avalonia.Controls public static readonly StyledProperty BackgroundProperty = Border.BackgroundProperty.AddOwner(); + /// + /// Defines the property. + /// + public static readonly StyledProperty PaddingProperty = + Decorator.PaddingProperty.AddOwner(); + // TODO: Define these attached properties elsewhere (e.g. on a Text class) and AddOwner // them into TextBlock. @@ -29,7 +36,7 @@ namespace Avalonia.Controls public static readonly AttachedProperty FontFamilyProperty = AvaloniaProperty.RegisterAttached( nameof(FontFamily), - defaultValue: FontFamily.Default, + defaultValue: FontFamily.Default, inherits: true); /// @@ -110,16 +117,13 @@ namespace Avalonia.Controls static TextBlock() { ClipToBoundsProperty.OverrideDefaultValue(true); - AffectsRender( - BackgroundProperty, - ForegroundProperty, - FontWeightProperty, - FontSizeProperty, - FontStyleProperty); Observable.Merge( TextProperty.Changed, + ForegroundProperty.Changed, TextAlignmentProperty.Changed, + TextWrappingProperty.Changed, + TextTrimmingProperty.Changed, FontSizeProperty.Changed, FontStyleProperty.Changed, FontWeightProperty.Changed @@ -145,6 +149,15 @@ namespace Avalonia.Controls } } + /// + /// Gets or sets the padding to place around the . + /// + public Thickness Padding + { + get { return GetValue(PaddingProperty); } + set { SetValue(PaddingProperty, value); } + } + /// /// Gets or sets a brush used to paint the control's background. /// @@ -363,7 +376,9 @@ namespace Avalonia.Controls context.FillRectangle(background, new Rect(Bounds.Size)); } - TextLayout?.Draw(context.PlatformImpl, new Point()); + var padding = Padding; + + TextLayout?.Draw(context.PlatformImpl, new Point(padding.Left, padding.Top)); } /// @@ -412,6 +427,10 @@ namespace Avalonia.Controls return new Size(); } + var padding = Padding; + + availableSize = availableSize.Deflate(padding); + if (_constraint != availableSize) { InvalidateTextLayout(); From 6c948af49d1e8eefda98a1a9c3dec1ec17878582 Mon Sep 17 00:00:00 2001 From: Benedikt Schroeder Date: Tue, 10 Mar 2020 09:28:24 +0100 Subject: [PATCH 10/24] Fix TextWrapping --- src/Avalonia.Controls/TextBlock.cs | 6 ++++- .../TextFormatting/SimpleTextFormatter.cs | 27 ++++++++++++------- .../Media/TextFormatting/TextLayout.cs | 15 +++++++---- 3 files changed, 33 insertions(+), 15 deletions(-) diff --git a/src/Avalonia.Controls/TextBlock.cs b/src/Avalonia.Controls/TextBlock.cs index 560c1952ac..a7ec6457fa 100644 --- a/src/Avalonia.Controls/TextBlock.cs +++ b/src/Avalonia.Controls/TextBlock.cs @@ -118,6 +118,8 @@ namespace Avalonia.Controls { ClipToBoundsProperty.OverrideDefaultValue(true); + AffectsRender(BackgroundProperty); + Observable.Merge( TextProperty.Changed, ForegroundProperty.Changed, @@ -438,7 +440,9 @@ namespace Avalonia.Controls _constraint = availableSize; - return TextLayout?.Bounds.Size ?? Size.Empty; + var measuredSize = TextLayout?.Bounds.Size ?? Size.Empty; + + return measuredSize.Inflate(padding); } protected override void OnAttachedToLogicalTree(LogicalTreeAttachmentEventArgs e) diff --git a/src/Avalonia.Visuals/Media/TextFormatting/SimpleTextFormatter.cs b/src/Avalonia.Visuals/Media/TextFormatting/SimpleTextFormatter.cs index e84242c628..30d513386e 100644 --- a/src/Avalonia.Visuals/Media/TextFormatting/SimpleTextFormatter.cs +++ b/src/Avalonia.Visuals/Media/TextFormatting/SimpleTextFormatter.cs @@ -201,18 +201,17 @@ namespace Avalonia.Media.TextFormatting var availableWidth = paragraphWidth; var currentWidth = 0.0; var runIndex = 0; + var length = 0; while (runIndex < textRuns.Count) { var currentRun = textRuns[runIndex]; - currentWidth += currentRun.GlyphRun.Bounds.Width; - - if (currentWidth > availableWidth) + if (currentWidth + currentRun.GlyphRun.Bounds.Width > availableWidth) { - var measuredLength = MeasureText(currentRun, paragraphWidth); + var measuredLength = MeasureText(currentRun, paragraphWidth - currentWidth); - if (measuredLength < text.End) + if (measuredLength < currentRun.Text.Length) { var currentBreakPosition = -1; @@ -241,15 +240,19 @@ namespace Avalonia.Media.TextFormatting } } - var splitResult = SplitTextRuns(textRuns, measuredLength); + length += measuredLength; + + var splitResult = SplitTextRuns(textRuns, length); var textLineMetrics = TextLineMetrics.Create(splitResult.First, paragraphWidth, paragraphProperties.TextAlignment); - return new SimpleTextLine(text.Take(measuredLength), splitResult.First, textLineMetrics); + return new SimpleTextLine(text.Take(length), splitResult.First, textLineMetrics); } - availableWidth -= currentRun.GlyphRun.Bounds.Width; + currentWidth += currentRun.GlyphRun.Bounds.Width; + + length += currentRun.GlyphRun.Characters.Length; runIndex++; } @@ -281,12 +284,18 @@ namespace Avalonia.Media.TextFormatting if (measuredWidth + advance > availableWidth) { + index--; break; } measuredWidth += advance; } + if(index < 0) + { + return 0; + } + var cluster = textRun.GlyphRun.GlyphClusters[index]; var characterHit = textRun.GlyphRun.FindNearestCharacterHit(cluster, out _); @@ -355,7 +364,7 @@ namespace Avalonia.Media.TextFormatting continue; } - var firstCount = currentRun.GlyphRun.Characters.Length > 1 ? i + 1 : i; + var firstCount = currentRun.GlyphRun.Characters.Length >= 1 ? i + 1 : i; var first = new ShapedTextRun[firstCount]; diff --git a/src/Avalonia.Visuals/Media/TextFormatting/TextLayout.cs b/src/Avalonia.Visuals/Media/TextFormatting/TextLayout.cs index 0c9013e6f7..2ef7322eaa 100644 --- a/src/Avalonia.Visuals/Media/TextFormatting/TextLayout.cs +++ b/src/Avalonia.Visuals/Media/TextFormatting/TextLayout.cs @@ -233,6 +233,11 @@ namespace Avalonia.Media.TextFormatting var textLine = TextFormatter.Current.FormatLine(textSource, 0, MaxWidth, _paragraphProperties); + if (!double.IsPositiveInfinity(MaxHeight) && bottom + textLine.LineMetrics.Size.Height > MaxHeight) + { + break; + } + UpdateBounds(textLine, ref left, ref right, ref bottom); textLines.Add(textLine); @@ -253,17 +258,17 @@ namespace Avalonia.Media.TextFormatting { var emptyTextLine = CreateEmptyTextLine(currentPosition); + if (!double.IsPositiveInfinity(MaxHeight) && bottom + emptyTextLine.LineMetrics.Size.Height > MaxHeight) + { + break; + } + UpdateBounds(emptyTextLine, ref left, ref right, ref bottom); textLines.Add(emptyTextLine); break; } - - if (!double.IsPositiveInfinity(MaxHeight) && MaxHeight < Bounds.Height) - { - break; - } } Bounds = new Rect(left, 0, right, bottom); From 55b4a1930b4541273b2b37c928d42a5bbf9f2b63 Mon Sep 17 00:00:00 2001 From: Benedikt Schroeder Date: Tue, 10 Mar 2020 15:19:11 +0100 Subject: [PATCH 11/24] Cleanup invalidation of measure and render --- src/Avalonia.Controls/TextBlock.cs | 25 ++++++++++++------- .../Media/TextFormatting/TextLayout.cs | 1 + 2 files changed, 17 insertions(+), 9 deletions(-) diff --git a/src/Avalonia.Controls/TextBlock.cs b/src/Avalonia.Controls/TextBlock.cs index a7ec6457fa..0278360ba5 100644 --- a/src/Avalonia.Controls/TextBlock.cs +++ b/src/Avalonia.Controls/TextBlock.cs @@ -2,7 +2,6 @@ // Licensed under the MIT license. See licence.md file in the project root for full license information. using System.Reactive.Linq; -using Avalonia.Layout; using Avalonia.LogicalTree; using Avalonia.Media; using Avalonia.Media.TextFormatting; @@ -118,7 +117,16 @@ namespace Avalonia.Controls { ClipToBoundsProperty.OverrideDefaultValue(true); - AffectsRender(BackgroundProperty); + AffectsRender( + BackgroundProperty, ForegroundProperty, FontSizeProperty, + FontWeightProperty, FontStyleProperty, TextWrappingProperty, + TextTrimmingProperty, TextAlignmentProperty, FontFamilyProperty, + TextDecorationsProperty, TextProperty, PaddingProperty); + + AffectsMeasure( + FontSizeProperty, FontWeightProperty, FontStyleProperty, + FontFamilyProperty, TextTrimmingProperty, TextProperty, + PaddingProperty); Observable.Merge( TextProperty.Changed, @@ -128,8 +136,11 @@ namespace Avalonia.Controls TextTrimmingProperty.Changed, FontSizeProperty.Changed, FontStyleProperty.Changed, - FontWeightProperty.Changed - ).AddClassHandler((x, _) => x.OnTextPropertiesChanged()); + FontWeightProperty.Changed, + FontFamilyProperty.Changed, + TextDecorationsProperty.Changed, + PaddingProperty.Changed + ).AddClassHandler((x, _) => x.InvalidateTextLayout()); } /// @@ -448,13 +459,9 @@ namespace Avalonia.Controls protected override void OnAttachedToLogicalTree(LogicalTreeAttachmentEventArgs e) { base.OnAttachedToLogicalTree(e); - InvalidateTextLayout(); - InvalidateMeasure(); - } - private void OnTextPropertiesChanged() - { InvalidateTextLayout(); + InvalidateMeasure(); } } diff --git a/src/Avalonia.Visuals/Media/TextFormatting/TextLayout.cs b/src/Avalonia.Visuals/Media/TextFormatting/TextLayout.cs index 2ef7322eaa..3f0cf7c680 100644 --- a/src/Avalonia.Visuals/Media/TextFormatting/TextLayout.cs +++ b/src/Avalonia.Visuals/Media/TextFormatting/TextLayout.cs @@ -235,6 +235,7 @@ namespace Avalonia.Media.TextFormatting if (!double.IsPositiveInfinity(MaxHeight) && bottom + textLine.LineMetrics.Size.Height > MaxHeight) { + currentPosition = _text.Length; break; } From d6d3704e8b2ecfca44621bdaf913256c04424fb3 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Wed, 11 Mar 2020 11:59:52 +0100 Subject: [PATCH 12/24] Update ncrunch config. Ignore samples. --- .ncrunch/BindingDemo.v3.ncrunchproject | 5 +++++ .ncrunch/RenderDemo.v3.ncrunchproject | 5 +++++ .ncrunch/VirtualizationDemo.v3.ncrunchproject | 5 +++++ 3 files changed, 15 insertions(+) create mode 100644 .ncrunch/BindingDemo.v3.ncrunchproject create mode 100644 .ncrunch/RenderDemo.v3.ncrunchproject create mode 100644 .ncrunch/VirtualizationDemo.v3.ncrunchproject diff --git a/.ncrunch/BindingDemo.v3.ncrunchproject b/.ncrunch/BindingDemo.v3.ncrunchproject new file mode 100644 index 0000000000..319cd523ce --- /dev/null +++ b/.ncrunch/BindingDemo.v3.ncrunchproject @@ -0,0 +1,5 @@ + + + True + + \ No newline at end of file diff --git a/.ncrunch/RenderDemo.v3.ncrunchproject b/.ncrunch/RenderDemo.v3.ncrunchproject new file mode 100644 index 0000000000..319cd523ce --- /dev/null +++ b/.ncrunch/RenderDemo.v3.ncrunchproject @@ -0,0 +1,5 @@ + + + True + + \ No newline at end of file diff --git a/.ncrunch/VirtualizationDemo.v3.ncrunchproject b/.ncrunch/VirtualizationDemo.v3.ncrunchproject new file mode 100644 index 0000000000..319cd523ce --- /dev/null +++ b/.ncrunch/VirtualizationDemo.v3.ncrunchproject @@ -0,0 +1,5 @@ + + + True + + \ No newline at end of file From 44c5b4340245d27bcd81d8696c51726ffc275269 Mon Sep 17 00:00:00 2001 From: ahopper Date: Wed, 11 Mar 2020 11:34:44 +0000 Subject: [PATCH 13/24] Reduce deferred renderer enumerator allocations --- src/Avalonia.Visuals/Rendering/DeferredRenderer.cs | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/Avalonia.Visuals/Rendering/DeferredRenderer.cs b/src/Avalonia.Visuals/Rendering/DeferredRenderer.cs index b065079564..b01dda4209 100644 --- a/src/Avalonia.Visuals/Rendering/DeferredRenderer.cs +++ b/src/Avalonia.Visuals/Rendering/DeferredRenderer.cs @@ -355,15 +355,19 @@ namespace Avalonia.Rendering node.BeginRender(context, isLayerRoot); - foreach (var operation in node.DrawOperations) + var drawOperations = node.DrawOperations; + for (int i = 0; i < drawOperations.Count; i++) { + var operation = drawOperations[i]; _currentDraw = operation; operation.Item.Render(context); _currentDraw = null; } - foreach (var child in node.Children) + var children = node.Children; + for (int i = 0; i < children.Count; i++) { + var child = children[i]; Render(context, (VisualNode)child, layer, clipBounds); } From 9207d439c4fc8f9ca7213ff5c36b9601c023db58 Mon Sep 17 00:00:00 2001 From: ahopper Date: Wed, 11 Mar 2020 14:45:24 +0000 Subject: [PATCH 14/24] local copies of .Count --- src/Avalonia.Visuals/Rendering/DeferredRenderer.cs | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/Avalonia.Visuals/Rendering/DeferredRenderer.cs b/src/Avalonia.Visuals/Rendering/DeferredRenderer.cs index b01dda4209..f11bd76a7b 100644 --- a/src/Avalonia.Visuals/Rendering/DeferredRenderer.cs +++ b/src/Avalonia.Visuals/Rendering/DeferredRenderer.cs @@ -356,7 +356,8 @@ namespace Avalonia.Rendering node.BeginRender(context, isLayerRoot); var drawOperations = node.DrawOperations; - for (int i = 0; i < drawOperations.Count; i++) + var drawOperationsCount = drawOperations.Count; + for (int i = 0; i < drawOperationsCount; i++) { var operation = drawOperations[i]; _currentDraw = operation; @@ -365,7 +366,8 @@ namespace Avalonia.Rendering } var children = node.Children; - for (int i = 0; i < children.Count; i++) + var childrenCount = children.Count; + for (int i = 0; i < childrenCount; i++) { var child = children[i]; Render(context, (VisualNode)child, layer, clipBounds); From a429159c3c32bca2b278763b02ebaede05148ab6 Mon Sep 17 00:00:00 2001 From: Deadpikle Date: Wed, 11 Mar 2020 17:20:52 -0400 Subject: [PATCH 15/24] Add SelectedText to TextBox --- src/Avalonia.Controls/TextBox.cs | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/src/Avalonia.Controls/TextBox.cs b/src/Avalonia.Controls/TextBox.cs index a438d7380b..943a73b427 100644 --- a/src/Avalonia.Controls/TextBox.cs +++ b/src/Avalonia.Controls/TextBox.cs @@ -275,6 +275,22 @@ namespace Avalonia.Controls } } + public string SelectedText + { + get { return GetSelection(); } + set + { + if (value == null) + { + return; + } + + _undoRedoHelper.Snapshot(); + HandleTextInput(value); + _undoRedoHelper.Snapshot(); + } + } + /// /// Gets or sets the horizontal alignment of the content within the control. /// From 47f487e768443813ea5e892bfa2e3ad8a6f0609a Mon Sep 17 00:00:00 2001 From: Deadpikle Date: Wed, 11 Mar 2020 17:27:06 -0400 Subject: [PATCH 16/24] Add SelectedText unit tests --- .../TextBoxTests.cs | 43 +++++++++++++++++++ 1 file changed, 43 insertions(+) diff --git a/tests/Avalonia.Controls.UnitTests/TextBoxTests.cs b/tests/Avalonia.Controls.UnitTests/TextBoxTests.cs index 225eca17b2..14251454ea 100644 --- a/tests/Avalonia.Controls.UnitTests/TextBoxTests.cs +++ b/tests/Avalonia.Controls.UnitTests/TextBoxTests.cs @@ -385,6 +385,49 @@ namespace Avalonia.Controls.UnitTests Assert.True(target.SelectionEnd <= "123".Length); } } + + [Fact] + public void SelectedText_Changes_OnSelectionChange() + { + using (UnitTestApplication.Start(Services)) + { + var target = new TextBox + { + Template = CreateTemplate(), + Text = "0123456789" + }; + + Assert.True(target.SelectedText == ""); + + target.SelectionStart = 2; + target.SelectionEnd = 4; + + Assert.True(target.SelectedText == "23"); + } + } + + [Fact] + public void SelectedText_EditsText() + { + using (UnitTestApplication.Start(Services)) + { + var target = new TextBox + { + Template = CreateTemplate(), + Text = "0123" + }; + + target.SelectedText = "AA"; + Assert.True(target.Text == "AA0123"); + + target.SelectionStart = 1; + target.SelectionEnd = 3; + target.SelectedText = "BB"; + + Assert.True(target.Text == "ABB123"); + } + } + [Fact] public void CoerceCaretIndex_Doesnt_Cause_Exception_with_malformed_line_ending() { From 5d007606bad8a8d9509129f1b4f7e9d7d43aeadd Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Thu, 12 Mar 2020 11:04:43 +0100 Subject: [PATCH 17/24] Rework HasSystemDecorations. Make it a direct property that is backed by `SystemDecorations`. Moved the changed handlers to `OnPropertyChanged`. --- src/Avalonia.Controls/Window.cs | 62 +++++++++++++++++++++++---------- 1 file changed, 43 insertions(+), 19 deletions(-) diff --git a/src/Avalonia.Controls/Window.cs b/src/Avalonia.Controls/Window.cs index 6154db9f8a..f4a3f05f03 100644 --- a/src/Avalonia.Controls/Window.cs +++ b/src/Avalonia.Controls/Window.cs @@ -2,19 +2,19 @@ // Licensed under the MIT license. See licence.md file in the project root for full license information. using System; +using System.ComponentModel; +using System.Linq; using System.Reactive.Linq; using System.Threading.Tasks; using Avalonia.Controls.Platform; +using Avalonia.Data; using Avalonia.Input; +using Avalonia.Interactivity; using Avalonia.Layout; using Avalonia.Media; using Avalonia.Platform; using Avalonia.Styling; -using System.Collections.Generic; -using System.Linq; using JetBrains.Annotations; -using System.ComponentModel; -using Avalonia.Interactivity; namespace Avalonia.Controls { @@ -81,8 +81,11 @@ namespace Avalonia.Controls /// Enables or disables system window decorations (title bar, buttons, etc) /// [Obsolete("Use SystemDecorationsProperty instead")] - public static readonly StyledProperty HasSystemDecorationsProperty = - AvaloniaProperty.Register(nameof(HasSystemDecorations), true); + public static readonly DirectProperty HasSystemDecorationsProperty = + AvaloniaProperty.RegisterDirect( + nameof(HasSystemDecorations), + o => o.HasSystemDecorations, + (o, v) => o.HasSystemDecorations = v); /// /// Defines the property. @@ -152,15 +155,6 @@ namespace Avalonia.Controls { BackgroundProperty.OverrideDefaultValue(typeof(Window), Brushes.White); TitleProperty.Changed.AddClassHandler((s, e) => s.PlatformImpl?.SetTitle((string)e.NewValue)); - HasSystemDecorationsProperty.Changed.AddClassHandler( - (s, e) => s.SetValue(SystemDecorationsProperty, (bool)e.NewValue ? SystemDecorations.Full : SystemDecorations.None)); - SystemDecorationsProperty.Changed.AddClassHandler( - (s, e) => - { - s.PlatformImpl?.SetSystemDecorations((SystemDecorations)e.NewValue); - s.SetValue(HasSystemDecorationsProperty, (SystemDecorations)e.NewValue != SystemDecorations.None); - }); - ShowInTaskbarProperty.Changed.AddClassHandler((w, e) => w.PlatformImpl?.ShowTaskbarIcon((bool)e.NewValue)); IconProperty.Changed.AddClassHandler((s, e) => s.PlatformImpl?.SetIcon(((WindowIcon)e.NewValue)?.PlatformImpl)); @@ -169,7 +163,7 @@ namespace Avalonia.Controls WindowStateProperty.Changed.AddClassHandler( (w, e) => { if (w.PlatformImpl != null) w.PlatformImpl.WindowState = (WindowState)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))); @@ -224,12 +218,20 @@ namespace Avalonia.Controls /// /// Enables or disables system window decorations (title bar, buttons, etc) /// - /// [Obsolete("Use SystemDecorations instead")] public bool HasSystemDecorations { - get { return GetValue(HasSystemDecorationsProperty); } - set { SetValue(HasSystemDecorationsProperty, value); } + get => SystemDecorations == SystemDecorations.Full; + set + { + var oldValue = HasSystemDecorations; + + if (oldValue != value) + { + SystemDecorations = value ? SystemDecorations.Full : SystemDecorations.None; + RaisePropertyChanged(HasSystemDecorationsProperty, oldValue, value); + } + } } /// @@ -628,5 +630,27 @@ namespace Avalonia.Controls /// event needs to be raised. /// protected virtual void OnClosing(CancelEventArgs e) => Closing?.Invoke(this, e); + + protected override void OnPropertyChanged( + AvaloniaProperty property, + Optional oldValue, + BindingValue newValue, + BindingPriority priority) + { + if (property == SystemDecorationsProperty) + { + var typedNewValue = newValue.GetValueOrDefault(); + + PlatformImpl?.SetSystemDecorations(typedNewValue); + + var o = oldValue.GetValueOrDefault() == SystemDecorations.Full; + var n = typedNewValue == SystemDecorations.Full; + + if (o != n) + { + RaisePropertyChanged(HasSystemDecorationsProperty, o, n); + } + } + } } } From f98ee717f50c125176212f85b01d89c7fdf7c7fc Mon Sep 17 00:00:00 2001 From: Deadpikle Date: Thu, 12 Mar 2020 10:32:45 -0400 Subject: [PATCH 18/24] Adjusted TextBox right-click behavior WPF does not allow dragging to change selection for right clicks Furthermore, it only changes the selection on mouse up of right click, not mouse down, and only if the user clicked outside the current selection. This commit adjusts TextBox selection behavior to work like WPF --- src/Avalonia.Controls/TextBox.cs | 24 +++++++++++++++++++++--- 1 file changed, 21 insertions(+), 3 deletions(-) diff --git a/src/Avalonia.Controls/TextBox.cs b/src/Avalonia.Controls/TextBox.cs index a438d7380b..f9dd98634c 100644 --- a/src/Avalonia.Controls/TextBox.cs +++ b/src/Avalonia.Controls/TextBox.cs @@ -684,11 +684,12 @@ namespace Avalonia.Controls protected override void OnPointerPressed(PointerPressedEventArgs e) { var point = e.GetPosition(_presenter); - var index = CaretIndex = _presenter.GetCaretIndex(point); + var index = _presenter.GetCaretIndex(point); var text = Text; - if (text != null && e.MouseButton == MouseButton.Left) + if (text != null && e.GetCurrentPoint(this).Properties.IsLeftButtonPressed) { + CaretIndex = index; switch (e.ClickCount) { case 1: @@ -714,7 +715,8 @@ namespace Avalonia.Controls protected override void OnPointerMoved(PointerEventArgs e) { - if (_presenter != null && e.Pointer.Captured == _presenter) + // selection should not change during pointer move if the user right clicks + if (_presenter != null && e.Pointer.Captured == _presenter && !e.GetCurrentPoint(this).Properties.IsRightButtonPressed) { var point = e.GetPosition(_presenter); @@ -727,6 +729,22 @@ namespace Avalonia.Controls { if (_presenter != null && e.Pointer.Captured == _presenter) { + if (e.InitialPressMouseButton == MouseButton.Right) + { + var point = e.GetPosition(_presenter); + var caretIndex = _presenter.GetCaretIndex(point); + + // see if mouse clicked inside current selection + // if it did not, we change the selection to where the user clicked + var firstSelection = Math.Min(SelectionStart, SelectionEnd); + var lastSelection = Math.Max(SelectionStart, SelectionEnd); + var didClickInSelection = SelectionStart != SelectionEnd && + caretIndex >= firstSelection && caretIndex <= lastSelection; + if (!didClickInSelection) + { + CaretIndex = SelectionEnd = SelectionStart = caretIndex; + } + } e.Pointer.Capture(null); } } From 1f18bd3e66e79f55fdaa9bc04479843e75d0cee7 Mon Sep 17 00:00:00 2001 From: Deadpikle Date: Thu, 12 Mar 2020 10:41:33 -0400 Subject: [PATCH 19/24] Only calculate caret index on pointer pressed if left mouse down --- src/Avalonia.Controls/TextBox.cs | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/Avalonia.Controls/TextBox.cs b/src/Avalonia.Controls/TextBox.cs index f9dd98634c..c4bfb5f1b1 100644 --- a/src/Avalonia.Controls/TextBox.cs +++ b/src/Avalonia.Controls/TextBox.cs @@ -683,13 +683,12 @@ namespace Avalonia.Controls protected override void OnPointerPressed(PointerPressedEventArgs e) { - var point = e.GetPosition(_presenter); - var index = _presenter.GetCaretIndex(point); var text = Text; if (text != null && e.GetCurrentPoint(this).Properties.IsLeftButtonPressed) { - CaretIndex = index; + var point = e.GetPosition(_presenter); + var index = CaretIndex = _presenter.GetCaretIndex(point); switch (e.ClickCount) { case 1: From 3ea20ab7d095f08dc821bd31e1540fa50a94852a Mon Sep 17 00:00:00 2001 From: Dariusz Komosinski Date: Sun, 15 Mar 2020 19:32:46 +0100 Subject: [PATCH 20/24] Avoid allocating extra delegates when initializing and deinitializing StyleClassActivator. --- .../Styling/Activators/StyleClassActivator.cs | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/src/Avalonia.Styling/Styling/Activators/StyleClassActivator.cs b/src/Avalonia.Styling/Styling/Activators/StyleClassActivator.cs index c884b6ac43..7906a29cb5 100644 --- a/src/Avalonia.Styling/Styling/Activators/StyleClassActivator.cs +++ b/src/Avalonia.Styling/Styling/Activators/StyleClassActivator.cs @@ -14,6 +14,7 @@ namespace Avalonia.Styling.Activators { private readonly IList _match; private readonly IAvaloniaReadOnlyList _classes; + private NotifyCollectionChangedEventHandler? _classesChangedHandler; public StyleClassActivator(IAvaloniaReadOnlyList classes, IList match) { @@ -21,6 +22,9 @@ namespace Avalonia.Styling.Activators _match = match; } + private NotifyCollectionChangedEventHandler ClassesChangedHandler => + _classesChangedHandler ??= ClassesChanged; + public static bool AreClassesMatching(IReadOnlyList classes, IList toMatch) { int remainingMatches = toMatch.Count; @@ -51,16 +55,15 @@ namespace Avalonia.Styling.Activators return remainingMatches == 0; } - protected override void Initialize() { PublishNext(IsMatching()); - _classes.CollectionChanged += ClassesChanged; + _classes.CollectionChanged += ClassesChangedHandler; } protected override void Deinitialize() { - _classes.CollectionChanged -= ClassesChanged; + _classes.CollectionChanged -= ClassesChangedHandler; } private void ClassesChanged(object sender, NotifyCollectionChangedEventArgs e) From dc93d14a1b09a21ab457c6d8f47827fb76d9a320 Mon Sep 17 00:00:00 2001 From: Dariusz Komosinski Date: Sun, 15 Mar 2020 20:17:11 +0100 Subject: [PATCH 21/24] Return IReadOnlyList from most of the AvaloniaPropertyRegistry calls so users are not forced into allocating enumerators. --- src/Avalonia.Base/AvaloniaObject.cs | 6 +- src/Avalonia.Base/AvaloniaPropertyRegistry.cs | 60 +++++++++++++++---- 2 files changed, 52 insertions(+), 14 deletions(-) diff --git a/src/Avalonia.Base/AvaloniaObject.cs b/src/Avalonia.Base/AvaloniaObject.cs index 88b99cd99a..51062f1568 100644 --- a/src/Avalonia.Base/AvaloniaObject.cs +++ b/src/Avalonia.Base/AvaloniaObject.cs @@ -80,8 +80,12 @@ namespace Avalonia _inheritanceParent?.RemoveInheritanceChild(this); _inheritanceParent = value; - foreach (var property in AvaloniaPropertyRegistry.Instance.GetRegisteredInherited(GetType())) + var properties = AvaloniaPropertyRegistry.Instance.GetRegisteredInherited(GetType()); + var propertiesCount = properties.Count; + + for (var i = 0; i < propertiesCount; i++) { + var property = properties[i]; if (valuestore?.IsSet(property) == true) { // If local value set there can be no change. diff --git a/src/Avalonia.Base/AvaloniaPropertyRegistry.cs b/src/Avalonia.Base/AvaloniaPropertyRegistry.cs index 29ab10278b..11bd58d05c 100644 --- a/src/Avalonia.Base/AvaloniaPropertyRegistry.cs +++ b/src/Avalonia.Base/AvaloniaPropertyRegistry.cs @@ -3,9 +3,7 @@ using System; using System.Collections.Generic; -using System.Linq; using System.Runtime.CompilerServices; -using Avalonia.Data; namespace Avalonia { @@ -49,7 +47,7 @@ namespace Avalonia /// /// The type. /// A collection of definitions. - public IEnumerable GetRegistered(Type type) + public IReadOnlyList GetRegistered(Type type) { Contract.Requires(type != null); @@ -83,7 +81,7 @@ namespace Avalonia /// /// The type. /// A collection of definitions. - public IEnumerable GetRegisteredAttached(Type type) + public IReadOnlyList GetRegisteredAttached(Type type) { Contract.Requires(type != null); @@ -114,7 +112,7 @@ namespace Avalonia /// /// The type. /// A collection of definitions. - public IEnumerable GetRegisteredDirect(Type type) + public IReadOnlyList GetRegisteredDirect(Type type) { Contract.Requires(type != null); @@ -145,7 +143,7 @@ namespace Avalonia /// /// The type. /// A collection of definitions. - public IEnumerable GetRegisteredInherited(Type type) + public IReadOnlyList GetRegisteredInherited(Type type) { Contract.Requires(type != null); @@ -157,16 +155,27 @@ namespace Avalonia result = new List(); var visited = new HashSet(); - foreach (var property in GetRegistered(type)) + var registered = GetRegistered(type); + var registeredCount = registered.Count; + + for (var i = 0; i < registeredCount; i++) { + var property = registered[i]; + if (property.Inherits) { result.Add(property); visited.Add(property); } } - foreach (var property in GetRegisteredAttached(type)) + + var registeredAttached = GetRegisteredAttached(type); + var registeredAttachedCount = registeredAttached.Count; + + for (var i = 0; i < registeredAttachedCount; i++) { + var property = registeredAttached[i]; + if (property.Inherits) { if (!visited.Contains(property)) @@ -185,7 +194,7 @@ namespace Avalonia /// /// The object. /// A collection of definitions. - public IEnumerable GetRegistered(IAvaloniaObject o) + public IReadOnlyList GetRegistered(IAvaloniaObject o) { Contract.Requires(o != null); @@ -229,8 +238,13 @@ namespace Avalonia throw new InvalidOperationException("Attached properties not supported."); } - foreach (AvaloniaProperty x in GetRegistered(type)) + var registered = GetRegistered(type); + var registeredCount = registered.Count; + + for (var i = 0; i < registeredCount; i++) { + AvaloniaProperty x = registered[i]; + if (x.Name == name) { return x; @@ -276,8 +290,13 @@ namespace Avalonia return property; } - foreach (var p in GetRegisteredDirect(o.GetType())) + var registeredDirect = GetRegisteredDirect(o.GetType()); + var registeredDirectCount = registeredDirect.Count; + + for (var i = 0; i < registeredDirectCount; i++) { + var p = registeredDirect[i]; + if (p == property) { return (DirectPropertyBase)p; @@ -308,8 +327,23 @@ namespace Avalonia Contract.Requires(type != null); Contract.Requires(property != null); - return Instance.GetRegistered(type).Any(x => x == property) || - Instance.GetRegisteredAttached(type).Any(x => x == property); + static bool ContainsProperty(IReadOnlyList properties, AvaloniaProperty property) + { + var propertiesCount = properties.Count; + + for (var i = 0; i < propertiesCount; i++) + { + if (properties[i] == property) + { + return true; + } + } + + return false; + } + + return ContainsProperty(Instance.GetRegistered(type), property) || + ContainsProperty(Instance.GetRegisteredAttached(type), property); } /// From 0e03dab3fecbf312e81578b23ae64acb45dff62e Mon Sep 17 00:00:00 2001 From: Dariusz Komosinski Date: Sun, 15 Mar 2020 20:18:15 +0100 Subject: [PATCH 22/24] Remove dead code. --- src/Avalonia.Base/AvaloniaPropertyRegistry.cs | 28 ------------------- 1 file changed, 28 deletions(-) diff --git a/src/Avalonia.Base/AvaloniaPropertyRegistry.cs b/src/Avalonia.Base/AvaloniaPropertyRegistry.cs index 11bd58d05c..7d3d5492b6 100644 --- a/src/Avalonia.Base/AvaloniaPropertyRegistry.cs +++ b/src/Avalonia.Base/AvaloniaPropertyRegistry.cs @@ -26,8 +26,6 @@ namespace Avalonia new Dictionary>(); private readonly Dictionary> _directCache = new Dictionary>(); - private readonly Dictionary> _initializedCache = - new Dictionary>(); private readonly Dictionary> _inheritedCache = new Dictionary>(); @@ -408,7 +406,6 @@ namespace Avalonia } _registeredCache.Clear(); - _initializedCache.Clear(); _inheritedCache.Clear(); } @@ -445,32 +442,7 @@ namespace Avalonia } _attachedCache.Clear(); - _initializedCache.Clear(); _inheritedCache.Clear(); } - - private readonly struct PropertyInitializationData - { - public AvaloniaProperty Property { get; } - public object Value { get; } - public bool IsDirect { get; } - public IDirectPropertyAccessor DirectAccessor { get; } - - public PropertyInitializationData(AvaloniaProperty property, IDirectPropertyAccessor directAccessor) - { - Property = property; - Value = null; - IsDirect = true; - DirectAccessor = directAccessor; - } - - public PropertyInitializationData(AvaloniaProperty property, IStyledPropertyAccessor styledAccessor, Type type) - { - Property = property; - Value = styledAccessor.GetDefaultValue(type); - IsDirect = false; - DirectAccessor = null; - } - } } } From 233d6639e39f27aaeea7c933e0bb484aadfdf08c Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Tue, 17 Mar 2020 14:39:46 +0100 Subject: [PATCH 23/24] Use more specific .NET core versions. To fix problem with CI failing on OSX. --- azure-pipelines.yml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/azure-pipelines.yml b/azure-pipelines.yml index 6a9f72039a..b70e0bf77f 100644 --- a/azure-pipelines.yml +++ b/azure-pipelines.yml @@ -35,16 +35,16 @@ jobs: vmImage: 'macOS-10.14' steps: - task: UseDotNet@2 - displayName: 'Use .NET Core SDK 3.1.x' + displayName: 'Use .NET Core SDK 3.1.101' inputs: packageType: sdk - version: 3.1.x + version: 3.1.101 - task: UseDotNet@2 - displayName: 'Use .NET Core Runtime 3.1.x' + displayName: 'Use .NET Core Runtime 3.1.1' inputs: packageType: runtime - version: 3.1.x + version: 3.1.1 - task: CmdLine@2 displayName: 'Install Mono 5.18' From a8663cd1f349cb3525e464e4fb6e51ef4973604e Mon Sep 17 00:00:00 2001 From: Deadpikle Date: Tue, 17 Mar 2020 12:27:05 -0400 Subject: [PATCH 24/24] Fix middle click being able to change selection --- src/Avalonia.Controls/TextBox.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Avalonia.Controls/TextBox.cs b/src/Avalonia.Controls/TextBox.cs index c4bfb5f1b1..cd5dff2c9a 100644 --- a/src/Avalonia.Controls/TextBox.cs +++ b/src/Avalonia.Controls/TextBox.cs @@ -715,7 +715,7 @@ namespace Avalonia.Controls protected override void OnPointerMoved(PointerEventArgs e) { // selection should not change during pointer move if the user right clicks - if (_presenter != null && e.Pointer.Captured == _presenter && !e.GetCurrentPoint(this).Properties.IsRightButtonPressed) + if (_presenter != null && e.Pointer.Captured == _presenter && e.GetCurrentPoint(this).Properties.IsLeftButtonPressed) { var point = e.GetPosition(_presenter);