From 571b7746eac6cc694aa586ec83d48ad5c9f4b9ac Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Sun, 22 Jan 2023 11:46:28 +0100 Subject: [PATCH 01/49] Added failing integration test. --- samples/IntegrationTestApp/MainWindow.axaml | 1 + .../IntegrationTestApp/MainWindow.axaml.cs | 2 ++ .../WindowTests_MacOS.cs | 24 ++++++++++++++++++- 3 files changed, 26 insertions(+), 1 deletion(-) diff --git a/samples/IntegrationTestApp/MainWindow.axaml b/samples/IntegrationTestApp/MainWindow.axaml index b116e4c789..7b1f0eddce 100644 --- a/samples/IntegrationTestApp/MainWindow.axaml +++ b/samples/IntegrationTestApp/MainWindow.axaml @@ -139,6 +139,7 @@ Maximized FullScreen + Can Resize diff --git a/samples/IntegrationTestApp/MainWindow.axaml.cs b/samples/IntegrationTestApp/MainWindow.axaml.cs index 3cd5350cce..087f25666b 100644 --- a/samples/IntegrationTestApp/MainWindow.axaml.cs +++ b/samples/IntegrationTestApp/MainWindow.axaml.cs @@ -66,11 +66,13 @@ namespace IntegrationTestApp var locationComboBox = this.GetControl("ShowWindowLocation"); var stateComboBox = this.GetControl("ShowWindowState"); var size = !string.IsNullOrWhiteSpace(sizeTextBox.Text) ? Size.Parse(sizeTextBox.Text) : (Size?)null; + var canResizeCheckBox = this.GetControl("ShowWindowCanResize"); var owner = (Window)this.GetVisualRoot()!; var window = new ShowWindowTest { WindowStartupLocation = (WindowStartupLocation)locationComboBox.SelectedIndex, + CanResize = canResizeCheckBox.IsChecked.Value, }; if (Application.Current?.ApplicationLifetime is IClassicDesktopStyleApplicationLifetime lifetime) diff --git a/tests/Avalonia.IntegrationTests.Appium/WindowTests_MacOS.cs b/tests/Avalonia.IntegrationTests.Appium/WindowTests_MacOS.cs index d9817ecdd1..06180be74a 100644 --- a/tests/Avalonia.IntegrationTests.Appium/WindowTests_MacOS.cs +++ b/tests/Avalonia.IntegrationTests.Appium/WindowTests_MacOS.cs @@ -323,11 +323,30 @@ namespace Avalonia.IntegrationTests.Appium secondaryWindow.GetChromeButtons().close.Click(); } - private IDisposable OpenWindow(PixelSize? size, ShowWindowMode mode, WindowStartupLocation location) + [PlatformTheory(TestPlatforms.MacOS)] + [InlineData(ShowWindowMode.NonOwned)] + [InlineData(ShowWindowMode.Owned)] + [InlineData(ShowWindowMode.Modal)] + public void Window_Has_Disabled_Zoom_Button_When_CanResize_Is_False(ShowWindowMode mode) + { + using (OpenWindow(null, mode, WindowStartupLocation.Manual, canResize: false)) + { + var secondaryWindow = GetWindow("SecondaryWindow"); + var (_, _, zoomButton) = secondaryWindow.GetChromeButtons(); + Assert.False(zoomButton.Enabled); + } + } + + private IDisposable OpenWindow( + PixelSize? size, + ShowWindowMode mode, + WindowStartupLocation location, + bool canResize = true) { var sizeTextBox = _session.FindElementByAccessibilityId("ShowWindowSize"); var modeComboBox = _session.FindElementByAccessibilityId("ShowWindowMode"); var locationComboBox = _session.FindElementByAccessibilityId("ShowWindowLocation"); + var canResizeCheckBox = _session.FindElementByAccessibilityId("ShowWindowCanResize"); var showButton = _session.FindElementByAccessibilityId("ShowWindow"); if (size.HasValue) @@ -338,6 +357,9 @@ namespace Avalonia.IntegrationTests.Appium locationComboBox.Click(); _session.FindElementByName(location.ToString()).SendClick(); + + if (canResizeCheckBox.GetIsChecked() != canResize) + canResizeCheckBox.Click(); return showButton.OpenWindowWithClick(); } From 2951b80c39601a8368cc902f76ef4f01bc9307c5 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Sat, 4 Feb 2023 21:26:19 +0100 Subject: [PATCH 02/49] Add additional failing integration tests. For problems introduced in #10153. --- .../WindowTests.cs | 20 ++++++++++++------- 1 file changed, 13 insertions(+), 7 deletions(-) diff --git a/tests/Avalonia.IntegrationTests.Appium/WindowTests.cs b/tests/Avalonia.IntegrationTests.Appium/WindowTests.cs index 7bb991aae6..23381c2e58 100644 --- a/tests/Avalonia.IntegrationTests.Appium/WindowTests.cs +++ b/tests/Avalonia.IntegrationTests.Appium/WindowTests.cs @@ -30,9 +30,9 @@ namespace Avalonia.IntegrationTests.Appium [Theory] [MemberData(nameof(StartupLocationData))] - public void StartupLocation(Size? size, ShowWindowMode mode, WindowStartupLocation location) + public void StartupLocation(Size? size, ShowWindowMode mode, WindowStartupLocation location, bool canResize) { - using var window = OpenWindow(size, mode, location); + using var window = OpenWindow(size, mode, location, canResize: canResize); var info = GetWindowInfo(); if (size.HasValue) @@ -230,10 +230,10 @@ namespace Avalonia.IntegrationTests.Appium Assert.Equal(new Rgba32(255, 0, 0), centerColor); } - public static TheoryData StartupLocationData() + public static TheoryData StartupLocationData() { var sizes = new Size?[] { null, new Size(400, 300) }; - var data = new TheoryData(); + var data = new TheoryData(); foreach (var size in sizes) { @@ -243,7 +243,8 @@ namespace Avalonia.IntegrationTests.Appium { if (!(location == WindowStartupLocation.CenterOwner && mode == ShowWindowMode.NonOwned)) { - data.Add(size, mode, location); + data.Add(size, mode, location, true); + data.Add(size, mode, location, false); } } } @@ -311,14 +312,16 @@ namespace Avalonia.IntegrationTests.Appium Size? size, ShowWindowMode mode, WindowStartupLocation location = WindowStartupLocation.Manual, - WindowState state = Controls.WindowState.Normal) + WindowState state = Controls.WindowState.Normal, + bool canResize = true) { var sizeTextBox = _session.FindElementByAccessibilityId("ShowWindowSize"); var modeComboBox = _session.FindElementByAccessibilityId("ShowWindowMode"); var locationComboBox = _session.FindElementByAccessibilityId("ShowWindowLocation"); var stateComboBox = _session.FindElementByAccessibilityId("ShowWindowState"); + var canResizeCheckBox = _session.FindElementByAccessibilityId("ShowWindowCanResize"); var showButton = _session.FindElementByAccessibilityId("ShowWindow"); - + if (size.HasValue) sizeTextBox.SendKeys($"{size.Value.Width}, {size.Value.Height}"); @@ -331,6 +334,9 @@ namespace Avalonia.IntegrationTests.Appium stateComboBox.Click(); _session.FindElementByAccessibilityId($"ShowWindowState{state}").SendClick(); + if (canResizeCheckBox.GetIsChecked() != canResize) + canResizeCheckBox.Click(); + return showButton.OpenWindowWithClick(); } From f41749170277d8e79c7a5e16af1a1db083705008 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Sat, 4 Feb 2023 22:44:24 +0100 Subject: [PATCH 03/49] Speed up integration tests. Don't change combo boxes that are already set to the correct value. --- .../WindowTests.cs | 21 +++++++++++++------ .../WindowTests_MacOS.cs | 14 +++++++++---- 2 files changed, 25 insertions(+), 10 deletions(-) diff --git a/tests/Avalonia.IntegrationTests.Appium/WindowTests.cs b/tests/Avalonia.IntegrationTests.Appium/WindowTests.cs index 23381c2e58..f3861ca267 100644 --- a/tests/Avalonia.IntegrationTests.Appium/WindowTests.cs +++ b/tests/Avalonia.IntegrationTests.Appium/WindowTests.cs @@ -325,14 +325,23 @@ namespace Avalonia.IntegrationTests.Appium if (size.HasValue) sizeTextBox.SendKeys($"{size.Value.Width}, {size.Value.Height}"); - modeComboBox.Click(); - _session.FindElementByName(mode.ToString()).SendClick(); + if (modeComboBox.GetComboBoxValue() != mode.ToString()) + { + modeComboBox.Click(); + _session.FindElementByName(mode.ToString()).SendClick(); + } - locationComboBox.Click(); - _session.FindElementByName(location.ToString()).SendClick(); + if (locationComboBox.GetComboBoxValue() != location.ToString()) + { + locationComboBox.Click(); + _session.FindElementByName(location.ToString()).SendClick(); + } - stateComboBox.Click(); - _session.FindElementByAccessibilityId($"ShowWindowState{state}").SendClick(); + if (stateComboBox.GetComboBoxValue() != state.ToString()) + { + stateComboBox.Click(); + _session.FindElementByAccessibilityId($"ShowWindowState{state}").SendClick(); + } if (canResizeCheckBox.GetIsChecked() != canResize) canResizeCheckBox.Click(); diff --git a/tests/Avalonia.IntegrationTests.Appium/WindowTests_MacOS.cs b/tests/Avalonia.IntegrationTests.Appium/WindowTests_MacOS.cs index 06180be74a..0839cbf183 100644 --- a/tests/Avalonia.IntegrationTests.Appium/WindowTests_MacOS.cs +++ b/tests/Avalonia.IntegrationTests.Appium/WindowTests_MacOS.cs @@ -352,11 +352,17 @@ namespace Avalonia.IntegrationTests.Appium if (size.HasValue) sizeTextBox.SendKeys($"{size.Value.Width}, {size.Value.Height}"); - modeComboBox.Click(); - _session.FindElementByName(mode.ToString()).SendClick(); + if (modeComboBox.GetComboBoxValue() != mode.ToString()) + { + modeComboBox.Click(); + _session.FindElementByName(mode.ToString()).SendClick(); + } - locationComboBox.Click(); - _session.FindElementByName(location.ToString()).SendClick(); + if (locationComboBox.GetComboBoxValue() != location.ToString()) + { + locationComboBox.Click(); + _session.FindElementByName(location.ToString()).SendClick(); + } if (canResizeCheckBox.GetIsChecked() != canResize) canResizeCheckBox.Click(); From e7a6d6fbc690b96af757199c98ff1f324f3d75ec Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Mon, 6 Feb 2023 12:00:48 +0100 Subject: [PATCH 04/49] Implement windowWillUseStandardFrame MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Seems that `NSWindow.isZoomed` returns the wrong result unless you implement this? ¯\_(ツ)_/¯ --- native/Avalonia.Native/src/OSX/AvnWindow.mm | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/native/Avalonia.Native/src/OSX/AvnWindow.mm b/native/Avalonia.Native/src/OSX/AvnWindow.mm index d3b7b4ede6..784072221d 100644 --- a/native/Avalonia.Native/src/OSX/AvnWindow.mm +++ b/native/Avalonia.Native/src/OSX/AvnWindow.mm @@ -223,6 +223,19 @@ } } +// From chromium: +// +// > The delegate or the window class should implement this method so that +// > -[NSWindow isZoomed] can be then determined by whether or not the current +// > window frame is equal to the zoomed frame. +// +// If we don't implement this, then isZoomed always returns true for a non- +// resizable window ¯\_(ツ)_/¯ +- (NSRect)windowWillUseStandardFrame:(NSWindow*)window + defaultFrame:(NSRect)newFrame { + return newFrame; +} + -(BOOL)canBecomeKeyWindow { if(_canBecomeKeyWindow) From 044a499db331b0d13e33d6427643bc1868470d30 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Mon, 6 Feb 2023 14:27:09 +0100 Subject: [PATCH 05/49] Added additional failing integration tests. --- tests/Avalonia.IntegrationTests.Appium/WindowTests.cs | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/tests/Avalonia.IntegrationTests.Appium/WindowTests.cs b/tests/Avalonia.IntegrationTests.Appium/WindowTests.cs index f3861ca267..ec24caa18c 100644 --- a/tests/Avalonia.IntegrationTests.Appium/WindowTests.cs +++ b/tests/Avalonia.IntegrationTests.Appium/WindowTests.cs @@ -63,9 +63,9 @@ namespace Avalonia.IntegrationTests.Appium [Theory] [MemberData(nameof(WindowStateData))] - public void WindowState(Size? size, ShowWindowMode mode, WindowState state) + public void WindowState(Size? size, ShowWindowMode mode, WindowState state, bool canResize) { - using var window = OpenWindow(size, mode, state: state); + using var window = OpenWindow(size, mode, state: state, canResize: canResize); try { @@ -253,10 +253,10 @@ namespace Avalonia.IntegrationTests.Appium return data; } - public static TheoryData WindowStateData() + public static TheoryData WindowStateData() { var sizes = new Size?[] { null, new Size(400, 300) }; - var data = new TheoryData(); + var data = new TheoryData(); foreach (var size in sizes) { @@ -274,7 +274,8 @@ namespace Avalonia.IntegrationTests.Appium mode != ShowWindowMode.NonOwned) continue; - data.Add(size, mode, state); + data.Add(size, mode, state, true); + data.Add(size, mode, state, false); } } } From 38aaadf92db4e4957a3ccab2365937c215e3c89a Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Mon, 6 Feb 2023 15:16:59 +0100 Subject: [PATCH 06/49] Use custom zoom logic when !_canResize. `[NSWindow setIsZoomed]` requires that the window is resizable by the user in order to work; when `canResize == false` this is not that case. --- native/Avalonia.Native/src/OSX/WindowImpl.mm | 16 ++++++---------- 1 file changed, 6 insertions(+), 10 deletions(-) diff --git a/native/Avalonia.Native/src/OSX/WindowImpl.mm b/native/Avalonia.Native/src/OSX/WindowImpl.mm index ce82f7d83f..af4f92524b 100644 --- a/native/Avalonia.Native/src/OSX/WindowImpl.mm +++ b/native/Avalonia.Native/src/OSX/WindowImpl.mm @@ -225,16 +225,12 @@ bool WindowImpl::IsZoomed() { } void WindowImpl::DoZoom() { - switch (_decorations) { - case SystemDecorationsNone: - case SystemDecorationsBorderOnly: - [Window setFrame:[Window screen].visibleFrame display:true]; - break; - - - case SystemDecorationsFull: - [Window performZoom:Window]; - break; + if (_decorations == SystemDecorationsNone || + _decorations == SystemDecorationsBorderOnly || + _canResize == false) { + [Window setFrame:[Window screen].visibleFrame display:true]; + } else { + [Window performZoom:Window]; } } From 5346344d12fa314c7317f90f03c8e798ee4ef6cc Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Mon, 6 Feb 2023 17:32:55 +0100 Subject: [PATCH 07/49] Don't call virtual method from ctor. And remove unneeded checks for already-existing `Window` (this method is always called from ctor). --- .../Avalonia.Native/src/OSX/WindowBaseImpl.mm | 19 +++++-------------- 1 file changed, 5 insertions(+), 14 deletions(-) diff --git a/native/Avalonia.Native/src/OSX/WindowBaseImpl.mm b/native/Avalonia.Native/src/OSX/WindowBaseImpl.mm index 038e9a048c..4d3768a4a8 100644 --- a/native/Avalonia.Native/src/OSX/WindowBaseImpl.mm +++ b/native/Avalonia.Native/src/OSX/WindowBaseImpl.mm @@ -580,21 +580,12 @@ void WindowBaseImpl::CleanNSWindow() { } } -void WindowBaseImpl::CreateNSWindow(bool isDialog) { - if (isDialog) { - if (![Window isKindOfClass:[AvnPanel class]]) { - CleanNSWindow(); - - Window = [[AvnPanel alloc] initWithParent:this contentRect:NSRect{0, 0, lastSize} styleMask:GetStyle()]; - - [Window setHidesOnDeactivate:false]; - } +void WindowBaseImpl::CreateNSWindow(bool usePanel) { + if (usePanel) { + Window = [[AvnPanel alloc] initWithParent:this contentRect:NSRect{0, 0, lastSize} styleMask:NSWindowStyleMaskBorderless]; + [Window setHidesOnDeactivate:false]; } else { - if (![Window isKindOfClass:[AvnWindow class]]) { - CleanNSWindow(); - - Window = [[AvnWindow alloc] initWithParent:this contentRect:NSRect{0, 0, lastSize} styleMask:GetStyle()]; - } + Window = [[AvnWindow alloc] initWithParent:this contentRect:NSRect{0, 0, lastSize} styleMask:NSWindowStyleMaskBorderless]; } } From b609ba58a8e445ff68ddbcd64d9ab3d9d4f6cb6c Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Mon, 6 Feb 2023 17:43:02 +0100 Subject: [PATCH 08/49] Disable zoom button when CanResize == false. To do this more easily, merged `HideOrShowTrafficLights` into a virtual `UpdateStyle`. They were always called together, and really _must_ be called together; this enforces that. --- .../Avalonia.Native/src/OSX/WindowBaseImpl.h | 3 +- native/Avalonia.Native/src/OSX/WindowImpl.h | 3 +- native/Avalonia.Native/src/OSX/WindowImpl.mm | 37 +++++++++---------- 3 files changed, 19 insertions(+), 24 deletions(-) diff --git a/native/Avalonia.Native/src/OSX/WindowBaseImpl.h b/native/Avalonia.Native/src/OSX/WindowBaseImpl.h index 4c2758f6c6..afa3a2956f 100644 --- a/native/Avalonia.Native/src/OSX/WindowBaseImpl.h +++ b/native/Avalonia.Native/src/OSX/WindowBaseImpl.h @@ -106,8 +106,7 @@ BEGIN_INTERFACE_MAP() protected: virtual NSWindowStyleMask GetStyle(); - - void UpdateStyle(); + virtual void UpdateStyle(); private: void CreateNSWindow (bool isDialog); diff --git a/native/Avalonia.Native/src/OSX/WindowImpl.h b/native/Avalonia.Native/src/OSX/WindowImpl.h index 3861aaf170..e7e9b7f1d8 100644 --- a/native/Avalonia.Native/src/OSX/WindowImpl.h +++ b/native/Avalonia.Native/src/OSX/WindowImpl.h @@ -41,8 +41,6 @@ BEGIN_INTERFACE_MAP() WindowImpl(IAvnWindowEvents* events, IAvnGlContext* gl); - void HideOrShowTrafficLights (); - virtual HRESULT Show (bool activate, bool isDialog) override; virtual HRESULT SetEnabled (bool enable) override; @@ -101,6 +99,7 @@ BEGIN_INTERFACE_MAP() protected: virtual NSWindowStyleMask GetStyle() override; + void UpdateStyle () override; private: void OnInitialiseNSWindow(); diff --git a/native/Avalonia.Native/src/OSX/WindowImpl.mm b/native/Avalonia.Native/src/OSX/WindowImpl.mm index af4f92524b..ae3e1eaf1d 100644 --- a/native/Avalonia.Native/src/OSX/WindowImpl.mm +++ b/native/Avalonia.Native/src/OSX/WindowImpl.mm @@ -30,19 +30,6 @@ WindowImpl::WindowImpl(IAvnWindowEvents *events, IAvnGlContext *gl) : WindowBase OnInitialiseNSWindow(); } -void WindowImpl::HideOrShowTrafficLights() { - if (Window == nil) { - return; - } - - bool wantsChrome = (_extendClientHints & AvnSystemChrome) || (_extendClientHints & AvnPreferSystemChrome); - bool hasTrafficLights = _isClientAreaExtended ? wantsChrome : _decorations == SystemDecorationsFull; - - [[Window standardWindowButton:NSWindowCloseButton] setHidden:!hasTrafficLights]; - [[Window standardWindowButton:NSWindowMiniaturizeButton] setHidden:!hasTrafficLights]; - [[Window standardWindowButton:NSWindowZoomButton] setHidden:!hasTrafficLights]; -} - void WindowImpl::OnInitialiseNSWindow(){ [GetWindowProtocol() setCanBecomeKeyWindow:true]; @@ -67,8 +54,6 @@ HRESULT WindowImpl::Show(bool activate, bool isDialog) { WindowBaseImpl::Show(activate, isDialog); GetWindowState(&_actualWindowState); - HideOrShowTrafficLights(); - return SetWindowState(_lastWindowState); } } @@ -257,8 +242,6 @@ HRESULT WindowImpl::SetDecorations(SystemDecorations value) { UpdateStyle(); - HideOrShowTrafficLights(); - switch (_decorations) { case SystemDecorationsNone: [Window setHasShadow:NO]; @@ -415,9 +398,6 @@ HRESULT WindowImpl::SetExtendClientArea(bool enable) { } [GetWindowProtocol() setIsExtended:enable]; - - HideOrShowTrafficLights(); - UpdateStyle(); } @@ -608,3 +588,20 @@ NSWindowStyleMask WindowImpl::GetStyle() { } return s; } + +void WindowImpl::UpdateStyle() { + WindowBaseImpl::UpdateStyle(); + + if (Window == nil) { + return; + } + + bool wantsChrome = (_extendClientHints & AvnSystemChrome) || (_extendClientHints & AvnPreferSystemChrome); + bool hasTrafficLights = _isClientAreaExtended ? wantsChrome : _decorations == SystemDecorationsFull; + + [[Window standardWindowButton:NSWindowCloseButton] setHidden:!hasTrafficLights]; + [[Window standardWindowButton:NSWindowMiniaturizeButton] setHidden:!hasTrafficLights]; + [[Window standardWindowButton:NSWindowZoomButton] setHidden:!hasTrafficLights]; + [[Window standardWindowButton:NSWindowZoomButton] setEnabled:_canResize]; +} + From d40041f02dd8ae5e53074e3b4c3e47b420ff9457 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Mon, 6 Feb 2023 19:13:59 +0100 Subject: [PATCH 09/49] GetStyle => abstract CalculateStyleMask. - Make naming more clear - it's not getting the style mask, it's calculating what it should be in order to update the mask - Make it abstract to prevent it being called from the ctor in future --- native/Avalonia.Native/src/OSX/PopupImpl.mm | 2 +- native/Avalonia.Native/src/OSX/WindowBaseImpl.h | 2 +- native/Avalonia.Native/src/OSX/WindowBaseImpl.mm | 10 +--------- native/Avalonia.Native/src/OSX/WindowImpl.h | 2 +- native/Avalonia.Native/src/OSX/WindowImpl.mm | 2 +- 5 files changed, 5 insertions(+), 13 deletions(-) diff --git a/native/Avalonia.Native/src/OSX/PopupImpl.mm b/native/Avalonia.Native/src/OSX/PopupImpl.mm index 9820a9f052..972d03d08c 100644 --- a/native/Avalonia.Native/src/OSX/PopupImpl.mm +++ b/native/Avalonia.Native/src/OSX/PopupImpl.mm @@ -29,7 +29,7 @@ private: [Window setLevel:NSPopUpMenuWindowLevel]; } protected: - virtual NSWindowStyleMask GetStyle() override + virtual NSWindowStyleMask CalculateStyleMask() override { return NSWindowStyleMaskBorderless; } diff --git a/native/Avalonia.Native/src/OSX/WindowBaseImpl.h b/native/Avalonia.Native/src/OSX/WindowBaseImpl.h index afa3a2956f..93decef136 100644 --- a/native/Avalonia.Native/src/OSX/WindowBaseImpl.h +++ b/native/Avalonia.Native/src/OSX/WindowBaseImpl.h @@ -105,7 +105,7 @@ BEGIN_INTERFACE_MAP() virtual void BringToFront (); protected: - virtual NSWindowStyleMask GetStyle(); + virtual NSWindowStyleMask CalculateStyleMask() = 0; virtual void UpdateStyle(); private: diff --git a/native/Avalonia.Native/src/OSX/WindowBaseImpl.mm b/native/Avalonia.Native/src/OSX/WindowBaseImpl.mm index 4d3768a4a8..59102e15a6 100644 --- a/native/Avalonia.Native/src/OSX/WindowBaseImpl.mm +++ b/native/Avalonia.Native/src/OSX/WindowBaseImpl.mm @@ -35,18 +35,14 @@ WindowBaseImpl::WindowBaseImpl(IAvnWindowBaseEvents *events, IAvnGlContext *gl, lastSize = NSSize { 100, 100 }; lastMaxSize = NSSize { CGFLOAT_MAX, CGFLOAT_MAX}; lastMinSize = NSSize { 0, 0 }; - lastMenu = nullptr; CreateNSWindow(usePanel); [Window setContentView:StandardContainer]; - [Window setStyleMask:NSWindowStyleMaskBorderless]; [Window setBackingType:NSBackingStoreBuffered]; - [Window setContentMinSize:lastMinSize]; [Window setContentMaxSize:lastMaxSize]; - [Window setOpaque:false]; } @@ -564,12 +560,8 @@ bool WindowBaseImpl::IsModal() { return false; } -NSWindowStyleMask WindowBaseImpl::GetStyle() { - return NSWindowStyleMaskBorderless; -} - void WindowBaseImpl::UpdateStyle() { - [Window setStyleMask:GetStyle()]; + [Window setStyleMask:CalculateStyleMask()]; } void WindowBaseImpl::CleanNSWindow() { diff --git a/native/Avalonia.Native/src/OSX/WindowImpl.h b/native/Avalonia.Native/src/OSX/WindowImpl.h index e7e9b7f1d8..9c684c77c4 100644 --- a/native/Avalonia.Native/src/OSX/WindowImpl.h +++ b/native/Avalonia.Native/src/OSX/WindowImpl.h @@ -98,7 +98,7 @@ BEGIN_INTERFACE_MAP() bool CanBecomeKeyWindow (); protected: - virtual NSWindowStyleMask GetStyle() override; + virtual NSWindowStyleMask CalculateStyleMask() override; void UpdateStyle () override; private: diff --git a/native/Avalonia.Native/src/OSX/WindowImpl.mm b/native/Avalonia.Native/src/OSX/WindowImpl.mm index ae3e1eaf1d..afd9c1a5ea 100644 --- a/native/Avalonia.Native/src/OSX/WindowImpl.mm +++ b/native/Avalonia.Native/src/OSX/WindowImpl.mm @@ -553,7 +553,7 @@ bool WindowImpl::IsOwned() { return _parent != nullptr; } -NSWindowStyleMask WindowImpl::GetStyle() { +NSWindowStyleMask WindowImpl::CalculateStyleMask() { unsigned long s = NSWindowStyleMaskBorderless; if(_actualWindowState == FullScreen) From 959b09c2434a285cead448fa8f22a6d282afb90d Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Tue, 7 Feb 2023 00:18:15 +0100 Subject: [PATCH 10/49] Skip flaky test for now. --- tests/Avalonia.IntegrationTests.Appium/WindowTests_MacOS.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/Avalonia.IntegrationTests.Appium/WindowTests_MacOS.cs b/tests/Avalonia.IntegrationTests.Appium/WindowTests_MacOS.cs index 0839cbf183..1933d10919 100644 --- a/tests/Avalonia.IntegrationTests.Appium/WindowTests_MacOS.cs +++ b/tests/Avalonia.IntegrationTests.Appium/WindowTests_MacOS.cs @@ -151,7 +151,7 @@ namespace Avalonia.IntegrationTests.Appium Assert.Equal("Normal", windowState.Text); } - [PlatformFact(TestPlatforms.MacOS)] + [PlatformFact(TestPlatforms.MacOS, Skip = "Flaky test, skip for now")] public void Does_Not_Switch_Space_From_FullScreen_To_Main_Desktop_When_FullScreen_Window_Clicked() { // Issue #9565 From 6a1910172c2a060f7ef67d875af03064db768d5f Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Tue, 7 Feb 2023 10:26:45 +0100 Subject: [PATCH 11/49] Move traffic lights logic into one place. --- native/Avalonia.Native/src/OSX/AvnWindow.mm | 4 ---- native/Avalonia.Native/src/OSX/WindowImpl.mm | 15 ++++++++++----- 2 files changed, 10 insertions(+), 9 deletions(-) diff --git a/native/Avalonia.Native/src/OSX/AvnWindow.mm b/native/Avalonia.Native/src/OSX/AvnWindow.mm index 784072221d..23abf1d53f 100644 --- a/native/Avalonia.Native/src/OSX/AvnWindow.mm +++ b/native/Avalonia.Native/src/OSX/AvnWindow.mm @@ -274,10 +274,6 @@ -(void) setEnabled:(bool)enable { _isEnabled = enable; - - [[self standardWindowButton:NSWindowCloseButton] setEnabled:enable]; - [[self standardWindowButton:NSWindowMiniaturizeButton] setEnabled:enable]; - [[self standardWindowButton:NSWindowZoomButton] setEnabled:enable]; } -(void)becomeKeyWindow diff --git a/native/Avalonia.Native/src/OSX/WindowImpl.mm b/native/Avalonia.Native/src/OSX/WindowImpl.mm index afd9c1a5ea..e4bbe24cb8 100644 --- a/native/Avalonia.Native/src/OSX/WindowImpl.mm +++ b/native/Avalonia.Native/src/OSX/WindowImpl.mm @@ -599,9 +599,14 @@ void WindowImpl::UpdateStyle() { bool wantsChrome = (_extendClientHints & AvnSystemChrome) || (_extendClientHints & AvnPreferSystemChrome); bool hasTrafficLights = _isClientAreaExtended ? wantsChrome : _decorations == SystemDecorationsFull; - [[Window standardWindowButton:NSWindowCloseButton] setHidden:!hasTrafficLights]; - [[Window standardWindowButton:NSWindowMiniaturizeButton] setHidden:!hasTrafficLights]; - [[Window standardWindowButton:NSWindowZoomButton] setHidden:!hasTrafficLights]; - [[Window standardWindowButton:NSWindowZoomButton] setEnabled:_canResize]; -} + NSButton* closeButton = [Window standardWindowButton:NSWindowCloseButton]; + NSButton* miniaturizeButton = [Window standardWindowButton:NSWindowMiniaturizeButton]; + NSButton* zoomButton = [Window standardWindowButton:NSWindowZoomButton]; + [closeButton setHidden:!hasTrafficLights]; + [closeButton setEnabled:_isEnabled]; + [miniaturizeButton setHidden:!hasTrafficLights]; + [miniaturizeButton setEnabled:_isEnabled]; + [zoomButton setHidden:!hasTrafficLights]; + [zoomButton setEnabled:_isEnabled && _canResize]; +} From a705f546bba1f3fb973aae2cdeb9ed8d08cf4f16 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Tue, 7 Feb 2023 11:46:18 +0100 Subject: [PATCH 12/49] Enable resizing during fullscreen transition. macOS seems to tie resizing of the NSView inside the window to the resizable style mask of the window somehow. If we programmatically transition a non-resizable window to fullscreen, the inner NSView's size isn't changed, so we need to make the window resizable during the fullscreen transition. Makes the final two failing `WindowState` integration tests pass. --- native/Avalonia.Native/src/OSX/WindowImpl.mm | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/native/Avalonia.Native/src/OSX/WindowImpl.mm b/native/Avalonia.Native/src/OSX/WindowImpl.mm index e4bbe24cb8..47e83f8d56 100644 --- a/native/Avalonia.Native/src/OSX/WindowImpl.mm +++ b/native/Avalonia.Native/src/OSX/WindowImpl.mm @@ -146,11 +146,13 @@ bool WindowImpl::CanBecomeKeyWindow() void WindowImpl::StartStateTransition() { _transitioningWindowState = true; + UpdateStyle(); } void WindowImpl::EndStateTransition() { _transitioningWindowState = false; - + UpdateStyle(); + // Ensure correct order of child windows after fullscreen transition. BringToFront(); } @@ -573,7 +575,7 @@ NSWindowStyleMask WindowImpl::CalculateStyleMask() { case SystemDecorationsFull: s = s | NSWindowStyleMaskTitled | NSWindowStyleMaskClosable; - if (_canResize && _isEnabled) { + if ((_canResize && _isEnabled) || _transitioningWindowState) { s = s | NSWindowStyleMaskResizable; } break; From 50a368eaa15e7a5756eeda2ec2fdf69d43e9f6da Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Tue, 7 Feb 2023 13:05:05 +0100 Subject: [PATCH 13/49] Skip flaky test for now. --- tests/Avalonia.IntegrationTests.Appium/WindowTests_MacOS.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/Avalonia.IntegrationTests.Appium/WindowTests_MacOS.cs b/tests/Avalonia.IntegrationTests.Appium/WindowTests_MacOS.cs index 1933d10919..47a471fafd 100644 --- a/tests/Avalonia.IntegrationTests.Appium/WindowTests_MacOS.cs +++ b/tests/Avalonia.IntegrationTests.Appium/WindowTests_MacOS.cs @@ -56,7 +56,7 @@ namespace Avalonia.IntegrationTests.Appium } } - [PlatformFact(TestPlatforms.MacOS)] + [PlatformFact(TestPlatforms.MacOS, Skip = "Flaky test, skip for now")] public void WindowOrder_Modal_Dialog_Stays_InFront_Of_Parent_When_Clicking_Resize_Grip() { var mainWindow = GetWindow("MainWindow"); From 784c380c604b6fd6a139abd7233aaf668661bfd0 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Tue, 7 Feb 2023 16:10:31 +0100 Subject: [PATCH 14/49] Don't change z-order of window exiting fullscreen. When a window exits fullscreen, its child windows need to be ordered, but we shouldn't touch the z-order of the window itself as this sometimes seemed to result in the parent window being shown over the child windows. Fixes flaky integration tests (hopefully). --- native/Avalonia.Native/src/OSX/WindowImpl.h | 1 + native/Avalonia.Native/src/OSX/WindowImpl.mm | 21 ++++++++++++------- .../WindowTests_MacOS.cs | 4 ++-- 3 files changed, 16 insertions(+), 10 deletions(-) diff --git a/native/Avalonia.Native/src/OSX/WindowImpl.h b/native/Avalonia.Native/src/OSX/WindowImpl.h index 9c684c77c4..29bb659039 100644 --- a/native/Avalonia.Native/src/OSX/WindowImpl.h +++ b/native/Avalonia.Native/src/OSX/WindowImpl.h @@ -102,6 +102,7 @@ protected: void UpdateStyle () override; private: + void ZOrderChildWindows(); void OnInitialiseNSWindow(); NSString *_lastTitle; }; diff --git a/native/Avalonia.Native/src/OSX/WindowImpl.mm b/native/Avalonia.Native/src/OSX/WindowImpl.mm index 47e83f8d56..4510d135dc 100644 --- a/native/Avalonia.Native/src/OSX/WindowImpl.mm +++ b/native/Avalonia.Native/src/OSX/WindowImpl.mm @@ -119,14 +119,19 @@ void WindowImpl::BringToFront() } [Window invalidateShadow]; + ZOrderChildWindows(); + } +} + +void WindowImpl::ZOrderChildWindows() +{ + for(auto iterator = _children.begin(); iterator != _children.end(); iterator++) + { + auto window = (*iterator)->Window; - for(auto iterator = _children.begin(); iterator != _children.end(); iterator++) - { - auto window = (*iterator)->Window; - - // #9565: Only bring window to front if it's on the currently active space - if ([window isOnActiveSpace]) - (*iterator)->BringToFront(); + // #9565: Only bring window to front if it's on the currently active space + if ([window isOnActiveSpace]) { + (*iterator)->BringToFront(); } } } @@ -154,7 +159,7 @@ void WindowImpl::EndStateTransition() { UpdateStyle(); // Ensure correct order of child windows after fullscreen transition. - BringToFront(); + ZOrderChildWindows(); } SystemDecorations WindowImpl::Decorations() { diff --git a/tests/Avalonia.IntegrationTests.Appium/WindowTests_MacOS.cs b/tests/Avalonia.IntegrationTests.Appium/WindowTests_MacOS.cs index 47a471fafd..0839cbf183 100644 --- a/tests/Avalonia.IntegrationTests.Appium/WindowTests_MacOS.cs +++ b/tests/Avalonia.IntegrationTests.Appium/WindowTests_MacOS.cs @@ -56,7 +56,7 @@ namespace Avalonia.IntegrationTests.Appium } } - [PlatformFact(TestPlatforms.MacOS, Skip = "Flaky test, skip for now")] + [PlatformFact(TestPlatforms.MacOS)] public void WindowOrder_Modal_Dialog_Stays_InFront_Of_Parent_When_Clicking_Resize_Grip() { var mainWindow = GetWindow("MainWindow"); @@ -151,7 +151,7 @@ namespace Avalonia.IntegrationTests.Appium Assert.Equal("Normal", windowState.Text); } - [PlatformFact(TestPlatforms.MacOS, Skip = "Flaky test, skip for now")] + [PlatformFact(TestPlatforms.MacOS)] public void Does_Not_Switch_Space_From_FullScreen_To_Main_Desktop_When_FullScreen_Window_Clicked() { // Issue #9565 From 1abb1abaf5f03ee87f66946acc3b2fc709a45d73 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Wed, 8 Feb 2023 10:42:17 +0100 Subject: [PATCH 15/49] Don't overwrite unrelated style mask bits. Fixes a problem where we were clearing the fullscreen flag erroneously during a fullscreen transition, and in general it's best to preserve flags we're not interested in controlling anyway. --- native/Avalonia.Native/src/OSX/WindowImpl.mm | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/native/Avalonia.Native/src/OSX/WindowImpl.mm b/native/Avalonia.Native/src/OSX/WindowImpl.mm index 4510d135dc..cf1ee6943d 100644 --- a/native/Avalonia.Native/src/OSX/WindowImpl.mm +++ b/native/Avalonia.Native/src/OSX/WindowImpl.mm @@ -561,13 +561,15 @@ bool WindowImpl::IsOwned() { } NSWindowStyleMask WindowImpl::CalculateStyleMask() { - unsigned long s = NSWindowStyleMaskBorderless; + // Use the current style mask and only clear the flags we're going to be modifying. + unsigned long s = [Window styleMask] & + ~(NSWindowStyleMaskFullSizeContentView | + NSWindowStyleMaskTitled | + NSWindowStyleMaskClosable | + NSWindowStyleMaskResizable | + NSWindowStyleMaskMiniaturizable | + NSWindowStyleMaskTexturedBackground); - if(_actualWindowState == FullScreen) - { - s |= NSWindowStyleMaskFullScreen; - } - switch (_decorations) { case SystemDecorationsNone: s = s | NSWindowStyleMaskFullSizeContentView; From 339b1e92bdc95c8b07b6c7f410744719883ffafe Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Wed, 8 Feb 2023 10:46:04 +0100 Subject: [PATCH 16/49] Add hack for strange win32 behavior. Fixes two failing integration tests on win32. --- src/Windows/Avalonia.Win32/WindowImpl.cs | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/src/Windows/Avalonia.Win32/WindowImpl.cs b/src/Windows/Avalonia.Win32/WindowImpl.cs index 1e0d92d442..22b43b1c18 100644 --- a/src/Windows/Avalonia.Win32/WindowImpl.cs +++ b/src/Windows/Avalonia.Win32/WindowImpl.cs @@ -1180,8 +1180,21 @@ namespace Avalonia.Win32 var y = monitorInfo.rcWork.top; var cx = Math.Abs(monitorInfo.rcWork.right - x); var cy = Math.Abs(monitorInfo.rcWork.bottom - y); + var style = (WindowStyles)GetWindowLong(_hwnd, (int)WindowLongParam.GWL_STYLE); - SetWindowPos(_hwnd, WindowPosZOrder.HWND_NOTOPMOST, x, y, cx, cy, SetWindowPosFlags.SWP_SHOWWINDOW); + if (!style.HasFlag(WindowStyles.WS_SIZEFRAME)) + { + // When calling SetWindowPos on a maximized window it automatically adjusts + // for "hidden" borders which are placed offscreen, EVEN IF THE WINDOW HAS + // NO BORDERS, meaning that the window is placed wrong when we have CanResize + // == false. Account for this here. + var borderThickness = BorderThickness; + x -= (int)borderThickness.Left; + cx += (int)borderThickness.Left + (int)borderThickness.Right; + cy += (int)borderThickness.Bottom; + } + + SetWindowPos(_hwnd, WindowPosZOrder.HWND_NOTOPMOST, x, y, cx, cy, SetWindowPosFlags.SWP_SHOWWINDOW | SetWindowPosFlags.SWP_FRAMECHANGED); } } } From 008cfce9a87cb64f33982fb90b10eb1e58ebaa1e Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Wed, 8 Feb 2023 10:46:12 +0100 Subject: [PATCH 17/49] Update ncrunch config. --- .ncrunch/Avalonia.UnitTests.v3.ncrunchproject | 5 +++++ .ncrunch/GpuInterop.v3.ncrunchproject | 5 +++++ 2 files changed, 10 insertions(+) create mode 100644 .ncrunch/Avalonia.UnitTests.v3.ncrunchproject create mode 100644 .ncrunch/GpuInterop.v3.ncrunchproject diff --git a/.ncrunch/Avalonia.UnitTests.v3.ncrunchproject b/.ncrunch/Avalonia.UnitTests.v3.ncrunchproject new file mode 100644 index 0000000000..cff5044edf --- /dev/null +++ b/.ncrunch/Avalonia.UnitTests.v3.ncrunchproject @@ -0,0 +1,5 @@ + + + False + + \ No newline at end of file diff --git a/.ncrunch/GpuInterop.v3.ncrunchproject b/.ncrunch/GpuInterop.v3.ncrunchproject new file mode 100644 index 0000000000..319cd523ce --- /dev/null +++ b/.ncrunch/GpuInterop.v3.ncrunchproject @@ -0,0 +1,5 @@ + + + True + + \ No newline at end of file From 4b0ff3be639755567691c0964f39ee37796ac9f7 Mon Sep 17 00:00:00 2001 From: DJGosnell Date: Thu, 9 Feb 2023 15:05:46 -0500 Subject: [PATCH 18/49] Initial work for adding caches for SKTextBlobBuilder, SKRoundRect, SKFont usages. Updates SKPaintCache for new caching base class. --- src/Skia/Avalonia.Skia/DrawingContextImpl.cs | 43 +++++++---- src/Skia/Avalonia.Skia/GeometryImpl.cs | 4 +- .../Gpu/OpenGl/GlRenderTarget.cs | 4 +- .../Avalonia.Skia/PlatformRenderInterface.cs | 47 ++++++------ src/Skia/Avalonia.Skia/SKCacheBase.cs | 72 +++++++++++++++++++ src/Skia/Avalonia.Skia/SKFontCache.cs | 13 ++++ src/Skia/Avalonia.Skia/SKPaintCache.cs | 58 ++------------- src/Skia/Avalonia.Skia/SKRoundRectCache.cs | 26 +++++++ .../Avalonia.Skia/SKTextBlobBuilderCache.cs | 13 ++++ src/Skia/Avalonia.Skia/TextShaperImpl.cs | 6 +- 10 files changed, 194 insertions(+), 92 deletions(-) create mode 100644 src/Skia/Avalonia.Skia/SKCacheBase.cs create mode 100644 src/Skia/Avalonia.Skia/SKFontCache.cs create mode 100644 src/Skia/Avalonia.Skia/SKRoundRectCache.cs create mode 100644 src/Skia/Avalonia.Skia/SKTextBlobBuilderCache.cs diff --git a/src/Skia/Avalonia.Skia/DrawingContextImpl.cs b/src/Skia/Avalonia.Skia/DrawingContextImpl.cs index ba646c64ee..a29cbb1cc3 100644 --- a/src/Skia/Avalonia.Skia/DrawingContextImpl.cs +++ b/src/Skia/Avalonia.Skia/DrawingContextImpl.cs @@ -34,9 +34,9 @@ namespace Avalonia.Skia private GRContext _grContext; public GRContext GrContext => _grContext; private ISkiaGpu _gpu; - private readonly SKPaint _strokePaint = SKPaintCache.Get(); - private readonly SKPaint _fillPaint = SKPaintCache.Get(); - private readonly SKPaint _boxShadowPaint = SKPaintCache.Get(); + private readonly SKPaint _strokePaint = SKPaintCache.Shared.Get(); + private readonly SKPaint _fillPaint = SKPaintCache.Shared.Get(); + private readonly SKPaint _boxShadowPaint = SKPaintCache.Shared.Get(); private static SKShader s_acrylicNoiseShader; private readonly ISkiaGpuRenderSession _session; private bool _leased = false; @@ -186,13 +186,13 @@ namespace Avalonia.Skia var s = sourceRect.ToSKRect(); var d = destRect.ToSKRect(); - var paint = SKPaintCache.Get(); + var paint = SKPaintCache.Shared.Get(); paint.Color = new SKColor(255, 255, 255, (byte)(255 * opacity * _currentOpacity)); paint.FilterQuality = bitmapInterpolationMode.ToSKFilterQuality(); paint.BlendMode = _currentBlendingMode.ToSKBlendMode(); drawableImage.Draw(this, s, d, paint); - SKPaintCache.ReturnReset(paint); + SKPaintCache.Shared.ReturnReset(paint); } /// @@ -535,7 +535,24 @@ namespace Avalonia.Skia { CheckLease(); Canvas.Save(); - Canvas.ClipRoundRect(clip.ToSKRoundRect(), antialias:true); + + // Get the rounded rectangle + var rc = clip.Rect.ToSKRect(); + + // Get a round rect from the cache. + var roundRect = SKRoundRectCache.Shared.Get(); + + roundRect.SetRectRadii(rc, + new[] + { + clip.RadiiTopLeft.ToSKPoint(), clip.RadiiTopRight.ToSKPoint(), + clip.RadiiBottomRight.ToSKPoint(), clip.RadiiBottomLeft.ToSKPoint(), + }); + + Canvas.ClipRoundRect(roundRect, antialias:true); + + // Should not need to reset as SetRectRadii overrides the values. + SKRoundRectCache.Shared.Return(roundRect); } /// @@ -569,9 +586,9 @@ namespace Avalonia.Skia try { // Return leased paints. - SKPaintCache.ReturnReset(_strokePaint); - SKPaintCache.ReturnReset(_fillPaint); - SKPaintCache.ReturnReset(_boxShadowPaint); + SKPaintCache.Shared.ReturnReset(_strokePaint); + SKPaintCache.Shared.ReturnReset(_fillPaint); + SKPaintCache.Shared.ReturnReset(_boxShadowPaint); if (_grContext != null) { @@ -633,7 +650,7 @@ namespace Avalonia.Skia { CheckLease(); - var paint = SKPaintCache.Get(); + var paint = SKPaintCache.Shared.Get(); Canvas.SaveLayer(paint); _maskStack.Push(CreatePaint(paint, mask, bounds.Size)); @@ -644,11 +661,11 @@ namespace Avalonia.Skia { CheckLease(); - var paint = SKPaintCache.Get(); + var paint = SKPaintCache.Shared.Get(); paint.BlendMode = SKBlendMode.DstIn; Canvas.SaveLayer(paint); - SKPaintCache.ReturnReset(paint); + SKPaintCache.Shared.ReturnReset(paint); PaintWrapper paintWrapper; using (paintWrapper = _maskStack.Pop()) @@ -656,7 +673,7 @@ namespace Avalonia.Skia Canvas.DrawPaint(paintWrapper.Paint); } // Return the paint wrapper's paint less the reset since the paint is already reset in the Dispose method above. - SKPaintCache.Return(paintWrapper.Paint); + SKPaintCache.Shared.Return(paintWrapper.Paint); Canvas.Restore(); diff --git a/src/Skia/Avalonia.Skia/GeometryImpl.cs b/src/Skia/Avalonia.Skia/GeometryImpl.cs index 15a3ebff40..51386d2a45 100644 --- a/src/Skia/Avalonia.Skia/GeometryImpl.cs +++ b/src/Skia/Avalonia.Skia/GeometryImpl.cs @@ -81,12 +81,12 @@ namespace Avalonia.Skia } else { - var paint = SKPaintCache.Get(); + var paint = SKPaintCache.Shared.Get(); paint.IsStroke = true; paint.StrokeWidth = strokeWidth; paint.GetFillPath(EffectivePath, strokePath); - SKPaintCache.ReturnReset(paint); + SKPaintCache.Shared.ReturnReset(paint); _pathCache.Cache(strokePath, strokeWidth, strokePath.TightBounds.ToAvaloniaRect()); } diff --git a/src/Skia/Avalonia.Skia/Gpu/OpenGl/GlRenderTarget.cs b/src/Skia/Avalonia.Skia/Gpu/OpenGl/GlRenderTarget.cs index 4b3c7a016d..25e004f4ef 100644 --- a/src/Skia/Avalonia.Skia/Gpu/OpenGl/GlRenderTarget.cs +++ b/src/Skia/Avalonia.Skia/Gpu/OpenGl/GlRenderTarget.cs @@ -13,7 +13,7 @@ namespace Avalonia.Skia { private readonly GRContext _grContext; private IGlPlatformSurfaceRenderTarget _surface; - + private static readonly SKSurfaceProperties _surfaceProperties = new SKSurfaceProperties(SKPixelGeometry.RgbHorizontal); public GlRenderTarget(GRContext grContext, IGlContext glContext, IGlPlatformSurface glSurface) { _grContext = grContext; @@ -92,7 +92,7 @@ namespace Avalonia.Skia var renderTarget = new GRBackendRenderTarget(size.Width, size.Height, samples, disp.StencilSize, glInfo); var surface = SKSurface.Create(_grContext, renderTarget, glSession.IsYFlipped ? GRSurfaceOrigin.TopLeft : GRSurfaceOrigin.BottomLeft, - colorType, new SKSurfaceProperties(SKPixelGeometry.RgbHorizontal)); + colorType, _surfaceProperties); success = true; diff --git a/src/Skia/Avalonia.Skia/PlatformRenderInterface.cs b/src/Skia/Avalonia.Skia/PlatformRenderInterface.cs index e795f3d304..8e9a19239b 100644 --- a/src/Skia/Avalonia.Skia/PlatformRenderInterface.cs +++ b/src/Skia/Avalonia.Skia/PlatformRenderInterface.cs @@ -76,13 +76,14 @@ namespace Avalonia.Skia } var fontRenderingEmSize = (float)glyphRun.FontRenderingEmSize; - var skFont = new SKFont(glyphTypeface.Typeface, fontRenderingEmSize) - { - Size = fontRenderingEmSize, - Edging = SKFontEdging.Alias, - Hinting = SKFontHinting.None, - LinearMetrics = true - }; + + var skFont = SKFontCache.Shared.Get(); + + skFont.Typeface = glyphTypeface.Typeface; + skFont.Size = fontRenderingEmSize; + skFont.Edging = SKFontEdging.Alias; + skFont.Hinting = SKFontHinting.None; + skFont.LinearMetrics = true; SKPath path = new SKPath(); @@ -101,6 +102,8 @@ namespace Avalonia.Skia currentX += glyphRun.GlyphInfos[i].GlyphAdvance; } + SKFontCache.Shared.Return(skFont); + return new StreamGeometryImpl(path); } @@ -224,20 +227,19 @@ namespace Avalonia.Skia var glyphTypefaceImpl = glyphTypeface as GlyphTypefaceImpl; - var font = new SKFont - { - LinearMetrics = true, - Subpixel = true, - Edging = SKFontEdging.SubpixelAntialias, - Hinting = SKFontHinting.Full, - Size = (float)fontRenderingEmSize, - Typeface = glyphTypefaceImpl.Typeface, - Embolden = (glyphTypefaceImpl.FontSimulations & FontSimulations.Bold) != 0, - SkewX = (glyphTypefaceImpl.FontSimulations & FontSimulations.Oblique) != 0 ? -0.2f : 0 - }; + var font = SKFontCache.Shared.Get(); + + font.LinearMetrics = true; + font.Subpixel = true; + font.Edging = SKFontEdging.SubpixelAntialias; + font.Hinting = SKFontHinting.Full; + font.Size = (float)fontRenderingEmSize; + font.Typeface = glyphTypefaceImpl.Typeface; + font.Embolden = (glyphTypefaceImpl.FontSimulations & FontSimulations.Bold) != 0; + font.SkewX = (glyphTypefaceImpl.FontSimulations & FontSimulations.Oblique) != 0 ? -0.2f : 0; - var builder = new SKTextBlobBuilder(); + var builder = SKTextBlobBuilderCache.Shared.Get(); var count = glyphInfos.Count; var runBuffer = builder.AllocatePositionedRun(font, count); @@ -245,6 +247,8 @@ namespace Avalonia.Skia var glyphSpan = runBuffer.GetGlyphSpan(); var positionSpan = runBuffer.GetPositionSpan(); + SKFontCache.Shared.Return(font); + var width = 0.0; for (int i = 0; i < count; i++) @@ -261,8 +265,11 @@ namespace Avalonia.Skia var scale = fontRenderingEmSize / glyphTypeface.Metrics.DesignEmHeight; var height = glyphTypeface.Metrics.LineSpacing * scale; + var skTextBlob = builder.Build(); + + SKTextBlobBuilderCache.Shared.Return(builder); - return new GlyphRunImpl(builder.Build(), new Size(width, height), baselineOrigin); + return new GlyphRunImpl(skTextBlob, new Size(width, height), baselineOrigin); } } } diff --git a/src/Skia/Avalonia.Skia/SKCacheBase.cs b/src/Skia/Avalonia.Skia/SKCacheBase.cs new file mode 100644 index 0000000000..e1e78cd081 --- /dev/null +++ b/src/Skia/Avalonia.Skia/SKCacheBase.cs @@ -0,0 +1,72 @@ +using System; +using System.Collections.Concurrent; +using SkiaSharp; + +namespace Avalonia.Skia +{ + /// + /// Cache base for Skia objects. + /// + internal abstract class SKCacheBase + where TCachedItem : IDisposable, new() + where TCache : new() + { + /// + /// Bag to hold the cached items. + /// + protected readonly ConcurrentBag Cache; + + /// + /// Shared cache. + /// + public static readonly TCache Shared = new TCache(); + + protected SKCacheBase() + { + Cache = new ConcurrentBag(); + } + + /// + /// Gets a cached item for usage. + /// + /// + /// If there is a available item in the cache, the cached item will be returned.. + /// Otherwise a new cached item will be created. + /// + /// + public TCachedItem Get() + { + if (!Cache.TryTake(out var item)) + { + item = new TCachedItem(); + } + + return item; + } + + /// + /// Returns the item for reuse later. + /// + /// + /// Do not use the item further. + /// Do not return the same item multiple times as that will break the cache. + /// + /// + public void Return(TCachedItem item) + { + Cache.Add(item); + } + + /// + /// Clears and disposes all cached items. + /// + public void Clear() + { + while (Cache.TryTake(out var item)) + { + item.Dispose(); + } + } + + } +} diff --git a/src/Skia/Avalonia.Skia/SKFontCache.cs b/src/Skia/Avalonia.Skia/SKFontCache.cs new file mode 100644 index 0000000000..348e085253 --- /dev/null +++ b/src/Skia/Avalonia.Skia/SKFontCache.cs @@ -0,0 +1,13 @@ +using System.Collections.Concurrent; +using SkiaSharp; + +namespace Avalonia.Skia +{ + /// + /// Cache for SKFonts. + /// + internal class SKFontCache : SKCacheBase + { + + } +} diff --git a/src/Skia/Avalonia.Skia/SKPaintCache.cs b/src/Skia/Avalonia.Skia/SKPaintCache.cs index 6588ab8da8..82c4dd23c7 100644 --- a/src/Skia/Avalonia.Skia/SKPaintCache.cs +++ b/src/Skia/Avalonia.Skia/SKPaintCache.cs @@ -6,46 +6,8 @@ namespace Avalonia.Skia /// /// Cache for SKPaints. /// - internal static class SKPaintCache + internal class SKPaintCache : SKCacheBase { - private static ConcurrentBag s_cachedPaints; - - static SKPaintCache() - { - s_cachedPaints = new ConcurrentBag(); - } - - /// - /// Gets a SKPaint for usage. - /// - /// - /// If a SKPaint is in the cache, that existing SKPaint will be returned. - /// Otherwise a new SKPaint will be created. - /// - /// - public static SKPaint Get() - { - if (!s_cachedPaints.TryTake(out var paint)) - { - paint = new SKPaint(); - } - - return paint; - } - - /// - /// Returns a SKPaint for reuse later. - /// - /// - /// Do not use the paint further. - /// Do not return the same paint multiple times as that will break the cache. - /// - /// - public static void Return(SKPaint paint) - { - s_cachedPaints.Add(paint); - } - /// /// Returns a SKPaint and resets it for reuse later. /// @@ -54,23 +16,11 @@ namespace Avalonia.Skia /// Do not return the same paint multiple times as that will break the cache. /// Uses SKPaint.Reset() for reuse later. /// - /// - public static void ReturnReset(SKPaint paint) + /// Paint to reset. + public void ReturnReset(SKPaint paint) { paint.Reset(); - s_cachedPaints.Add(paint); + Cache.Add(paint); } - - /// - /// Clears and disposes all cached paints. - /// - public static void Clear() - { - while (s_cachedPaints.TryTake(out var paint)) - { - paint.Dispose(); - } - } - } } diff --git a/src/Skia/Avalonia.Skia/SKRoundRectCache.cs b/src/Skia/Avalonia.Skia/SKRoundRectCache.cs new file mode 100644 index 0000000000..e164f97d6a --- /dev/null +++ b/src/Skia/Avalonia.Skia/SKRoundRectCache.cs @@ -0,0 +1,26 @@ +using System.Collections.Concurrent; +using SkiaSharp; + +namespace Avalonia.Skia +{ + /// + /// Cache for SKPaints. + /// + internal class SKRoundRectCache : SKCacheBase + { + /// + /// Returns a SKPaint and resets it for reuse later. + /// + /// + /// Do not use the rect further. + /// Do not return the same rect multiple times as that will break the cache. + /// Uses SKRoundRect.SetEmpty(); for reuse later. + /// + /// Rectangle to reset + public void ReturnReset(SKRoundRect rect) + { + rect.SetEmpty(); + Cache.Add(rect); + } + } +} diff --git a/src/Skia/Avalonia.Skia/SKTextBlobBuilderCache.cs b/src/Skia/Avalonia.Skia/SKTextBlobBuilderCache.cs new file mode 100644 index 0000000000..8c010ecb05 --- /dev/null +++ b/src/Skia/Avalonia.Skia/SKTextBlobBuilderCache.cs @@ -0,0 +1,13 @@ +using System.Collections.Concurrent; +using SkiaSharp; + +namespace Avalonia.Skia +{ + /// + /// Cache for SKTextBlobBuilder. + /// + internal class SKTextBlobBuilderCache : SKCacheBase + { + + } +} diff --git a/src/Skia/Avalonia.Skia/TextShaperImpl.cs b/src/Skia/Avalonia.Skia/TextShaperImpl.cs index e1a6b93692..a21038839c 100644 --- a/src/Skia/Avalonia.Skia/TextShaperImpl.cs +++ b/src/Skia/Avalonia.Skia/TextShaperImpl.cs @@ -1,5 +1,6 @@ using System; using System.Buffers; +using System.Collections.Concurrent; using System.Globalization; using System.Runtime.InteropServices; using Avalonia.Media.TextFormatting; @@ -13,6 +14,7 @@ namespace Avalonia.Skia { internal class TextShaperImpl : ITextShaperImpl { + private static readonly ConcurrentDictionary s_cachedLanguage = new(); public ShapedBuffer ShapeText(ReadOnlyMemory text, TextShaperOptions options) { var textSpan = text.Span; @@ -33,7 +35,9 @@ namespace Avalonia.Skia buffer.Direction = (bidiLevel & 1) == 0 ? Direction.LeftToRight : Direction.RightToLeft; - buffer.Language = new Language(culture ?? CultureInfo.CurrentCulture); + var usedCulture = culture ?? CultureInfo.CurrentCulture; + + buffer.Language = s_cachedLanguage.GetOrAdd(usedCulture.LCID, i => new Language(usedCulture)); var font = ((GlyphTypefaceImpl)typeface).Font; From b11786424daa1eafb4bb53edf83b57e4daca4117 Mon Sep 17 00:00:00 2001 From: DJGosnell Date: Thu, 9 Feb 2023 16:53:40 -0500 Subject: [PATCH 19/49] Cached round SKRoundRects created with DrawingContextImpl.DrawRectangle --- src/Skia/Avalonia.Skia/DrawingContextImpl.cs | 34 +++++++++++++++----- 1 file changed, 26 insertions(+), 8 deletions(-) diff --git a/src/Skia/Avalonia.Skia/DrawingContextImpl.cs b/src/Skia/Avalonia.Skia/DrawingContextImpl.cs index a29cbb1cc3..2bb6f1dc7e 100644 --- a/src/Skia/Avalonia.Skia/DrawingContextImpl.cs +++ b/src/Skia/Avalonia.Skia/DrawingContextImpl.cs @@ -315,15 +315,20 @@ namespace Avalonia.Skia var rc = rect.Rect.ToSKRect(); var isRounded = rect.IsRounded; var needRoundRect = rect.IsRounded; - using var skRoundRect = needRoundRect ? new SKRoundRect() : null; + SKRoundRect skRoundRect = null; if (needRoundRect) + { + skRoundRect = SKRoundRectCache.Shared.Get(); skRoundRect.SetRectRadii(rc, new[] { - rect.RadiiTopLeft.ToSKPoint(), rect.RadiiTopRight.ToSKPoint(), - rect.RadiiBottomRight.ToSKPoint(), rect.RadiiBottomLeft.ToSKPoint(), + rect.RadiiTopLeft.ToSKPoint(), + rect.RadiiTopRight.ToSKPoint(), + rect.RadiiBottomRight.ToSKPoint(), + rect.RadiiBottomLeft.ToSKPoint(), }); + } if (material != null) { @@ -332,6 +337,7 @@ namespace Avalonia.Skia if (isRounded) { Canvas.DrawRoundRect(skRoundRect, paint.Paint); + SKRoundRectCache.Shared.Return(skRoundRect); } else { @@ -356,14 +362,19 @@ namespace Avalonia.Skia var rc = rect.Rect.ToSKRect(); var isRounded = rect.IsRounded; var needRoundRect = rect.IsRounded || (boxShadows.HasInsetShadows); - using var skRoundRect = needRoundRect ? new SKRoundRect() : null; + SKRoundRect skRoundRect = null; if (needRoundRect) + { + skRoundRect = SKRoundRectCache.Shared.Get(); skRoundRect.SetRectRadii(rc, new[] { - rect.RadiiTopLeft.ToSKPoint(), rect.RadiiTopRight.ToSKPoint(), - rect.RadiiBottomRight.ToSKPoint(), rect.RadiiBottomLeft.ToSKPoint(), + rect.RadiiTopLeft.ToSKPoint(), + rect.RadiiTopRight.ToSKPoint(), + rect.RadiiBottomRight.ToSKPoint(), + rect.RadiiBottomLeft.ToSKPoint(), }); + } foreach (var boxShadow in boxShadows) { @@ -378,7 +389,8 @@ namespace Avalonia.Skia Canvas.Save(); if (isRounded) { - using var shadowRect = new SKRoundRect(skRoundRect); + var shadowRect = SKRoundRectCache.Shared.Get(); + shadowRect.SetRectRadii(skRoundRect!.Rect, skRoundRect.Radii); if (spread != 0) shadowRect.Inflate(spread, spread); Canvas.ClipRoundRect(skRoundRect, @@ -388,6 +400,7 @@ namespace Avalonia.Skia Transform = oldTransform * Matrix.CreateTranslation(boxShadow.OffsetX, boxShadow.OffsetY); Canvas.DrawRoundRect(shadowRect, shadow.Paint); Transform = oldTransform; + SKRoundRectCache.Shared.Return(shadowRect); } else { @@ -433,7 +446,8 @@ namespace Avalonia.Skia var outerRect = AreaCastingShadowInHole(rc, (float)boxShadow.Blur, spread, offsetX, offsetY); Canvas.Save(); - using var shadowRect = new SKRoundRect(skRoundRect); + var shadowRect = SKRoundRectCache.Shared.Get(); + shadowRect.SetRectRadii(skRoundRect!.Rect, skRoundRect.Radii); if (spread != 0) shadowRect.Deflate(spread, spread); Canvas.ClipRoundRect(skRoundRect, @@ -445,6 +459,7 @@ namespace Avalonia.Skia Canvas.DrawRoundRectDifference(outerRRect, shadowRect, shadow.Paint); Transform = oldTransform; Canvas.Restore(); + SKRoundRectCache.Shared.Return(shadowRect); } } } @@ -466,6 +481,9 @@ namespace Avalonia.Skia } } } + + if(isRounded) + SKRoundRectCache.Shared.Return(skRoundRect); } /// From 416ef6b601462f2eca229cfec1dd3481a8098aaa Mon Sep 17 00:00:00 2001 From: DJGosnell Date: Fri, 10 Feb 2023 18:45:20 -0500 Subject: [PATCH 20/49] Added two GetAndSetRadii methods. Added documentaiton. --- src/Skia/Avalonia.Skia/DrawingContextImpl.cs | 16 +---- src/Skia/Avalonia.Skia/SKRoundRectCache.cs | 76 +++++++++++++++++++- 2 files changed, 77 insertions(+), 15 deletions(-) diff --git a/src/Skia/Avalonia.Skia/DrawingContextImpl.cs b/src/Skia/Avalonia.Skia/DrawingContextImpl.cs index 2bb6f1dc7e..eededb2836 100644 --- a/src/Skia/Avalonia.Skia/DrawingContextImpl.cs +++ b/src/Skia/Avalonia.Skia/DrawingContextImpl.cs @@ -365,15 +365,7 @@ namespace Avalonia.Skia SKRoundRect skRoundRect = null; if (needRoundRect) { - skRoundRect = SKRoundRectCache.Shared.Get(); - skRoundRect.SetRectRadii(rc, - new[] - { - rect.RadiiTopLeft.ToSKPoint(), - rect.RadiiTopRight.ToSKPoint(), - rect.RadiiBottomRight.ToSKPoint(), - rect.RadiiBottomLeft.ToSKPoint(), - }); + skRoundRect = SKRoundRectCache.Shared.GetAndSetRadii(rc, rect); } foreach (var boxShadow in boxShadows) @@ -389,8 +381,7 @@ namespace Avalonia.Skia Canvas.Save(); if (isRounded) { - var shadowRect = SKRoundRectCache.Shared.Get(); - shadowRect.SetRectRadii(skRoundRect!.Rect, skRoundRect.Radii); + var shadowRect = SKRoundRectCache.Shared.GetAndSetRadii(skRoundRect!.Rect, skRoundRect.Radii); if (spread != 0) shadowRect.Inflate(spread, spread); Canvas.ClipRoundRect(skRoundRect, @@ -446,8 +437,7 @@ namespace Avalonia.Skia var outerRect = AreaCastingShadowInHole(rc, (float)boxShadow.Blur, spread, offsetX, offsetY); Canvas.Save(); - var shadowRect = SKRoundRectCache.Shared.Get(); - shadowRect.SetRectRadii(skRoundRect!.Rect, skRoundRect.Radii); + var shadowRect = SKRoundRectCache.Shared.GetAndSetRadii(skRoundRect!.Rect, skRoundRect.Radii); if (spread != 0) shadowRect.Deflate(spread, spread); Canvas.ClipRoundRect(skRoundRect, diff --git a/src/Skia/Avalonia.Skia/SKRoundRectCache.cs b/src/Skia/Avalonia.Skia/SKRoundRectCache.cs index e164f97d6a..8de9e65553 100644 --- a/src/Skia/Avalonia.Skia/SKRoundRectCache.cs +++ b/src/Skia/Avalonia.Skia/SKRoundRectCache.cs @@ -1,13 +1,73 @@ -using System.Collections.Concurrent; +using System.Buffers; +using System.Collections.Concurrent; +using System.Collections.Generic; using SkiaSharp; namespace Avalonia.Skia { /// - /// Cache for SKPaints. + /// Cache for SKRoundRectCache. /// internal class SKRoundRectCache : SKCacheBase { + /// + /// Cache for points to use for setting the radii. + /// + private readonly ConcurrentBag _radiiCache = new(); + + /// + /// Gets a cached SKRoundRect and sets it with the passed rectangle and Radii. + /// + /// Rectangle size to set the cached rectangle to. + /// Rounded rectangle to copy the radii from. + /// Configured rounded rectangle + public SKRoundRect GetAndSetRadii(in SKRect rectangle, in RoundedRect roundedRect) + { + if (!Cache.TryTake(out var item)) + { + item = new SKRoundRect(); + } + + // Try and acquire a cached point array. + if (!_radiiCache.TryTake(out var skArray)) + { + skArray = new SKPoint[4]; + } + + skArray[0].X = (float)roundedRect.RadiiTopLeft.X; + skArray[0].Y = (float)roundedRect.RadiiTopLeft.Y; + skArray[1].X = (float)roundedRect.RadiiTopRight.X; + skArray[1].Y = (float)roundedRect.RadiiTopRight.Y; + skArray[2].X = (float)roundedRect.RadiiBottomRight.X; + skArray[2].Y = (float)roundedRect.RadiiBottomRight.Y; + skArray[3].X = (float)roundedRect.RadiiBottomLeft.X; + skArray[3].Y = (float)roundedRect.RadiiBottomLeft.Y; + + item.SetRectRadii(rectangle, skArray); + + // Add the array back to the cache. + _radiiCache.Add(skArray); + + return item; + } + + /// + /// Gets a cached SKRoundRect and sets it with the passed rectangle and Radii. + /// + /// Rectangle size to set the cached rectangle to. + /// point array of radii. + /// Configured rounded rectangle + public SKRoundRect GetAndSetRadii(in SKRect rectangle, in SKPoint[] radii) + { + if (!Cache.TryTake(out var item)) + { + item = new SKRoundRect(); + } + + item.SetRectRadii(rectangle, radii); + + return item; + } /// /// Returns a SKPaint and resets it for reuse later. /// @@ -22,5 +82,17 @@ namespace Avalonia.Skia rect.SetEmpty(); Cache.Add(rect); } + + /// + /// Clears and disposes all cached items. + /// + public new void Clear() + { + base.Clear(); + + // Clear out the cache of SKPoint arrays. + _radiiCache.Clear(); + } + } } From 02906a623485971475cd2095987c8f8d3caa9cdf Mon Sep 17 00:00:00 2001 From: Daniil Pavliuchyk Date: Mon, 13 Feb 2023 18:04:22 +0200 Subject: [PATCH 21/49] Add SliderAutomationPeer --- .../Automation/Peers/SliderAutomationPeer.cs | 22 +++++++ src/Avalonia.Controls/Slider.cs | 6 ++ .../Automation/AutomationNode.cs | 65 ++++++------------- .../Automation/RootAutomationNode.cs | 35 +++++++++- 4 files changed, 81 insertions(+), 47 deletions(-) create mode 100644 src/Avalonia.Controls/Automation/Peers/SliderAutomationPeer.cs diff --git a/src/Avalonia.Controls/Automation/Peers/SliderAutomationPeer.cs b/src/Avalonia.Controls/Automation/Peers/SliderAutomationPeer.cs new file mode 100644 index 0000000000..42b15eec96 --- /dev/null +++ b/src/Avalonia.Controls/Automation/Peers/SliderAutomationPeer.cs @@ -0,0 +1,22 @@ +using Avalonia.Automation.Peers; + +namespace Avalonia.Controls.Automation.Peers +{ + public class SliderAutomationPeer : RangeBaseAutomationPeer + { + public SliderAutomationPeer(Slider owner) : base(owner) + { + } + + override protected string GetClassNameCore() + { + return "Slider"; + } + + override protected AutomationControlType GetAutomationControlTypeCore() + { + return AutomationControlType.Slider; + } + + } +} diff --git a/src/Avalonia.Controls/Slider.cs b/src/Avalonia.Controls/Slider.cs index 828bf2a1fb..7de726a932 100644 --- a/src/Avalonia.Controls/Slider.cs +++ b/src/Avalonia.Controls/Slider.cs @@ -10,6 +10,7 @@ using Avalonia.Interactivity; using Avalonia.Layout; using Avalonia.Utilities; using Avalonia.Automation; +using Avalonia.Controls.Automation.Peers; namespace Avalonia.Controls { @@ -380,6 +381,11 @@ namespace Avalonia.Controls } } + protected override AutomationPeer OnCreateAutomationPeer() + { + return new SliderAutomationPeer(this); + } + /// protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change) { diff --git a/src/Windows/Avalonia.Win32/Automation/AutomationNode.cs b/src/Windows/Avalonia.Win32/Automation/AutomationNode.cs index 29ab2cea3a..d35443b339 100644 --- a/src/Windows/Avalonia.Win32/Automation/AutomationNode.cs +++ b/src/Windows/Avalonia.Win32/Automation/AutomationNode.cs @@ -22,7 +22,6 @@ namespace Avalonia.Win32.Automation IRawElementProviderSimple, IRawElementProviderSimple2, IRawElementProviderFragment, - IRawElementProviderAdviseEvents, IInvokeProvider { private static Dictionary s_propertyMap = new Dictionary() @@ -50,14 +49,31 @@ namespace Avalonia.Win32.Automation new ConditionalWeakTable(); private readonly int[] _runtimeId; - private int _raiseFocusChanged; - private int _raisePropertyChanged; public AutomationNode(AutomationPeer peer) { _runtimeId = new int[] { 3, GetHashCode() }; Peer = peer; s_nodes.Add(peer, this); + peer.ChildrenChanged += Peer_ChildrenChanged; + peer.PropertyChanged += Peer_PropertyChanged; + } + + private void Peer_ChildrenChanged(object sender, EventArgs e) + { + ChildrenChanged(); + } + + private void Peer_PropertyChanged(object sender, AutomationPropertyChangedEventArgs e) + { + if (s_propertyMap.TryGetValue(e.Property, out var id)) + { + UiaCoreProviderApi.UiaRaiseAutomationPropertyChangedEvent( + this, + (int)id, + e.OldValue as IConvertible, + e.NewValue as IConvertible); + } } public AutomationPeer Peer { get; protected set; } @@ -89,14 +105,6 @@ namespace Avalonia.Win32.Automation 0); } - public void PropertyChanged(AutomationProperty property, object? oldValue, object? newValue) - { - if (_raisePropertyChanged > 0 && s_propertyMap.TryGetValue(property, out var id)) - { - UiaCoreProviderApi.UiaRaiseAutomationPropertyChangedEvent(this, (int)id, oldValue, newValue); - } - } - [return: MarshalAs(UnmanagedType.IUnknown)] public virtual object? GetPatternProvider(int patternId) { @@ -190,32 +198,6 @@ namespace Avalonia.Win32.Automation void IRawElementProviderSimple2.ShowContextMenu() => InvokeSync(() => Peer.ShowContextMenu()); void IInvokeProvider.Invoke() => InvokeSync((AAP.IInvokeProvider x) => x.Invoke()); - void IRawElementProviderAdviseEvents.AdviseEventAdded(int eventId, int[] properties) - { - switch ((UiaEventId)eventId) - { - case UiaEventId.AutomationPropertyChanged: - ++_raisePropertyChanged; - break; - case UiaEventId.AutomationFocusChanged: - ++_raiseFocusChanged; - break; - } - } - - void IRawElementProviderAdviseEvents.AdviseEventRemoved(int eventId, int[] properties) - { - switch ((UiaEventId)eventId) - { - case UiaEventId.AutomationPropertyChanged: - --_raisePropertyChanged; - break; - case UiaEventId.AutomationFocusChanged: - --_raiseFocusChanged; - break; - } - } - protected void InvokeSync(Action action) { if (Dispatcher.UIThread.CheckAccess()) @@ -268,15 +250,6 @@ namespace Avalonia.Win32.Automation throw new NotSupportedException(); } - protected void RaiseFocusChanged(AutomationNode? focused) - { - if (_raiseFocusChanged > 0) - { - UiaCoreProviderApi.UiaRaiseAutomationEvent( - focused, - (int)UiaEventId.AutomationFocusChanged); - } - } private AutomationNode? GetRoot() { diff --git a/src/Windows/Avalonia.Win32/Automation/RootAutomationNode.cs b/src/Windows/Avalonia.Win32/Automation/RootAutomationNode.cs index b732c4169f..1c6784798e 100644 --- a/src/Windows/Avalonia.Win32/Automation/RootAutomationNode.cs +++ b/src/Windows/Avalonia.Win32/Automation/RootAutomationNode.cs @@ -12,8 +12,11 @@ namespace Avalonia.Win32.Automation { [RequiresUnreferencedCode("Requires .NET COM interop")] internal class RootAutomationNode : AutomationNode, - IRawElementProviderFragmentRoot + IRawElementProviderFragmentRoot, + IRawElementProviderAdviseEvents { + private int _raiseFocusChanged; + public RootAutomationNode(AutomationPeer peer) : base(peer) { @@ -44,6 +47,36 @@ namespace Avalonia.Win32.Automation return GetOrCreate(focus); } + void IRawElementProviderAdviseEvents.AdviseEventAdded(int eventId, int[] properties) + { + switch ((UiaEventId)eventId) + { + case UiaEventId.AutomationFocusChanged: + ++_raiseFocusChanged; + break; + } + } + + void IRawElementProviderAdviseEvents.AdviseEventRemoved(int eventId, int[] properties) + { + switch ((UiaEventId)eventId) + { + case UiaEventId.AutomationFocusChanged: + --_raiseFocusChanged; + break; + } + } + + protected void RaiseFocusChanged(AutomationNode? focused) + { + if (_raiseFocusChanged > 0) + { + UiaCoreProviderApi.UiaRaiseAutomationEvent( + focused, + (int)UiaEventId.AutomationFocusChanged); + } + } + public void FocusChanged(object? sender, EventArgs e) { RaiseFocusChanged(GetOrCreate(Peer.GetFocus())); From 4a0a5e68d680a0c4ecf1de0ac69ee26c87b1b41b Mon Sep 17 00:00:00 2001 From: DJGosnell Date: Mon, 13 Feb 2023 13:26:16 -0500 Subject: [PATCH 22/49] Fix for netstandard2.0. --- src/Skia/Avalonia.Skia/SKRoundRectCache.cs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/Skia/Avalonia.Skia/SKRoundRectCache.cs b/src/Skia/Avalonia.Skia/SKRoundRectCache.cs index 8de9e65553..4be6171a93 100644 --- a/src/Skia/Avalonia.Skia/SKRoundRectCache.cs +++ b/src/Skia/Avalonia.Skia/SKRoundRectCache.cs @@ -91,8 +91,9 @@ namespace Avalonia.Skia base.Clear(); // Clear out the cache of SKPoint arrays. - _radiiCache.Clear(); + while (_radiiCache.TryTake(out var item)) + { + } } - } } From 37f5f50212cce6d4ee61d10f20cc6fcc309d1c21 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Wies=C5=82aw=20=C5=A0olt=C3=A9s?= Date: Mon, 13 Feb 2023 22:41:41 +0100 Subject: [PATCH 23/49] Update Layoutable.cs --- src/Avalonia.Base/Layout/Layoutable.cs | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/Avalonia.Base/Layout/Layoutable.cs b/src/Avalonia.Base/Layout/Layoutable.cs index 775b8adddd..f14ad3058a 100644 --- a/src/Avalonia.Base/Layout/Layoutable.cs +++ b/src/Avalonia.Base/Layout/Layoutable.cs @@ -323,12 +323,15 @@ namespace Avalonia.Layout set { SetValue(UseLayoutRoundingProperty, value); } } - internal Size? PreviousMeasure => _previousMeasure; + /// + /// Gets the available size passed in the previous layout pass, if any. + /// + public Size? PreviousMeasure => _previousMeasure; /// /// Gets the layout rect passed in the previous layout pass, if any. /// - internal Rect? PreviousArrange => _previousArrange; + public Rect? PreviousArrange => _previousArrange; /// /// Creates the visual children of the control, if necessary From ac21ab2fe69539522296a6d043ebbb98dcaeaaaf Mon Sep 17 00:00:00 2001 From: Daniil Pavliuchyk Date: Tue, 14 Feb 2023 15:06:13 +0200 Subject: [PATCH 24/49] Add Slider Automation Test --- samples/IntegrationTestApp/MainWindow.axaml | 3 ++ .../SliderTests.cs | 35 +++++++++++++++++++ 2 files changed, 38 insertions(+) create mode 100644 tests/Avalonia.IntegrationTests.Appium/SliderTests.cs diff --git a/samples/IntegrationTestApp/MainWindow.axaml b/samples/IntegrationTestApp/MainWindow.axaml index b116e4c789..8fd33061df 100644 --- a/samples/IntegrationTestApp/MainWindow.axaml +++ b/samples/IntegrationTestApp/MainWindow.axaml @@ -151,6 +151,9 @@ + + + diff --git a/tests/Avalonia.IntegrationTests.Appium/SliderTests.cs b/tests/Avalonia.IntegrationTests.Appium/SliderTests.cs new file mode 100644 index 0000000000..f6c50af59d --- /dev/null +++ b/tests/Avalonia.IntegrationTests.Appium/SliderTests.cs @@ -0,0 +1,35 @@ +using System; +using OpenQA.Selenium.Appium; +using OpenQA.Selenium.Interactions; +using Xunit; + +namespace Avalonia.IntegrationTests.Appium +{ + [Collection("Default")] + public class SliderTests + { + private readonly AppiumDriver _session; + + public SliderTests(TestAppFixture fixture) + { + _session = fixture.Session; + + var tabs = _session.FindElementByAccessibilityId("MainTabs"); + var tab = tabs.FindElementByName("Slider"); + tab.Click(); + } + + [Fact] + public void Changes_Value_When_Moving_Slider() + { + var slider = _session.FindElementByAccessibilityId("Slider2"); + + // slider.Text gets the Slider value + Assert.True(double.Parse(slider.Text) == 30); + + new Actions(_session).Click(slider).MoveByOffset(100, 0).Perform(); + + Assert.Equal(50, Math.Round(double.Parse(slider.Text))); + } + } +} From 4ad731ea60bc11bba5dc35e9d3ebd65ce4bd04bd Mon Sep 17 00:00:00 2001 From: Daniil Pavliuchyk Date: Tue, 14 Feb 2023 15:38:03 +0200 Subject: [PATCH 25/49] Fix nullability issues --- src/Windows/Avalonia.Win32/Automation/AutomationNode.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Windows/Avalonia.Win32/Automation/AutomationNode.cs b/src/Windows/Avalonia.Win32/Automation/AutomationNode.cs index d35443b339..0642331b74 100644 --- a/src/Windows/Avalonia.Win32/Automation/AutomationNode.cs +++ b/src/Windows/Avalonia.Win32/Automation/AutomationNode.cs @@ -59,12 +59,12 @@ namespace Avalonia.Win32.Automation peer.PropertyChanged += Peer_PropertyChanged; } - private void Peer_ChildrenChanged(object sender, EventArgs e) + private void Peer_ChildrenChanged(object? sender, EventArgs e) { ChildrenChanged(); } - private void Peer_PropertyChanged(object sender, AutomationPropertyChangedEventArgs e) + private void Peer_PropertyChanged(object? sender, AutomationPropertyChangedEventArgs e) { if (s_propertyMap.TryGetValue(e.Property, out var id)) { From 199d6678209308df971dc87d11fd03bc35618a11 Mon Sep 17 00:00:00 2001 From: Julien Lebosquain Date: Sun, 5 Feb 2023 11:21:29 +0100 Subject: [PATCH 26/49] Lazy initialized Win32 pointer info buffers --- src/Windows/Avalonia.Win32/WindowImpl.AppWndProc.cs | 5 +++++ src/Windows/Avalonia.Win32/WindowImpl.cs | 8 ++++---- 2 files changed, 9 insertions(+), 4 deletions(-) diff --git a/src/Windows/Avalonia.Win32/WindowImpl.AppWndProc.cs b/src/Windows/Avalonia.Win32/WindowImpl.AppWndProc.cs index d169ccfc1e..e8d2d8f0c5 100644 --- a/src/Windows/Avalonia.Win32/WindowImpl.AppWndProc.cs +++ b/src/Windows/Avalonia.Win32/WindowImpl.AppWndProc.cs @@ -793,6 +793,7 @@ namespace Avalonia.Win32 if (info.pointerType == PointerInputType.PT_TOUCH) { + s_historyTouchInfos ??= new POINTER_TOUCH_INFO[MaxPointerHistorySize]; if (GetPointerTouchInfoHistory(info.pointerId, ref historyCount, s_historyTouchInfos)) { for (int i = historyCount - 1; i >= 1; i--) @@ -804,6 +805,7 @@ namespace Avalonia.Win32 } else if (info.pointerType == PointerInputType.PT_PEN) { + s_historyPenInfos ??= new POINTER_PEN_INFO[MaxPointerHistorySize]; if (GetPointerPenInfoHistory(info.pointerId, ref historyCount, s_historyPenInfos)) { for (int i = historyCount - 1; i >= 1; i--) @@ -815,6 +817,7 @@ namespace Avalonia.Win32 } else { + s_historyInfos ??= new POINTER_INFO[MaxPointerHistorySize]; // Currently Windows does not return history info for mouse input, but we handle it just for case. if (GetPointerInfoHistory(info.pointerId, ref historyCount, s_historyInfos)) { @@ -837,6 +840,8 @@ namespace Avalonia.Win32 // To understand some of this code, please check MS docs: // https://learn.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-getmousemovepointsex#remarks + s_mouseHistoryInfos ??= new MOUSEMOVEPOINT[64]; + fixed (MOUSEMOVEPOINT* movePoints = s_mouseHistoryInfos) { var movePointCopy = movePoint; diff --git a/src/Windows/Avalonia.Win32/WindowImpl.cs b/src/Windows/Avalonia.Win32/WindowImpl.cs index 39593331c1..419cb57e87 100644 --- a/src/Windows/Avalonia.Win32/WindowImpl.cs +++ b/src/Windows/Avalonia.Win32/WindowImpl.cs @@ -100,10 +100,10 @@ namespace Avalonia.Win32 private const int MaxPointerHistorySize = 512; private static readonly PooledList s_intermediatePointsPooledList = new(); - private static readonly POINTER_TOUCH_INFO[] s_historyTouchInfos = new POINTER_TOUCH_INFO[MaxPointerHistorySize]; - private static readonly POINTER_PEN_INFO[] s_historyPenInfos = new POINTER_PEN_INFO[MaxPointerHistorySize]; - private static readonly POINTER_INFO[] s_historyInfos = new POINTER_INFO[MaxPointerHistorySize]; - private static readonly MOUSEMOVEPOINT[] s_mouseHistoryInfos = new MOUSEMOVEPOINT[64]; + private static POINTER_TOUCH_INFO[]? s_historyTouchInfos; + private static POINTER_PEN_INFO[]? s_historyPenInfos; + private static POINTER_INFO[]? s_historyInfos; + private static MOUSEMOVEPOINT[]? s_mouseHistoryInfos; public WindowImpl() { From e975468e9278b1f78fbb4a1afdb7334c301b41c3 Mon Sep 17 00:00:00 2001 From: Max Katz Date: Tue, 14 Feb 2023 09:23:26 -0500 Subject: [PATCH 27/49] Remove unused CanOpenRead/CanOpenWrite --- .../ControlCatalog/Pages/DialogsPage.xaml.cs | 35 ++++++++----------- .../Platform/Storage/AndroidStorageItem.cs | 6 +--- .../Platform/Storage/FileIO/BclStorageFile.cs | 6 +--- .../Platform/Storage/IStorageFile.cs | 12 +------ .../Screenshots/FilePickerHandler.cs | 4 --- .../Storage/BrowserStorageProvider.cs | 2 -- .../Avalonia.iOS/Storage/IOSStorageItem.cs | 6 +--- 7 files changed, 19 insertions(+), 52 deletions(-) diff --git a/samples/ControlCatalog/Pages/DialogsPage.xaml.cs b/samples/ControlCatalog/Pages/DialogsPage.xaml.cs index f7b020678d..5f116f95b6 100644 --- a/samples/ControlCatalog/Pages/DialogsPage.xaml.cs +++ b/samples/ControlCatalog/Pages/DialogsPage.xaml.cs @@ -223,7 +223,7 @@ namespace ControlCatalog.Pages ShowOverwritePrompt = false }); - if (file is not null && file.CanOpenWrite) + if (file is not null) { // Sync disposal of StreamWriter is not supported on WASM #if NET6_0_OR_GREATER @@ -298,31 +298,26 @@ namespace ControlCatalog.Pages if (item is IStorageFile file) { resultText += @$" - CanOpenRead: {file.CanOpenRead} - CanOpenWrite: {file.CanOpenWrite} Content: "; - if (file.CanOpenRead) - { #if NET6_0_OR_GREATER - await using var stream = await file.OpenReadAsync(); + await using var stream = await file.OpenReadAsync(); #else - using var stream = await file.OpenReadAsync(); + using var stream = await file.OpenReadAsync(); #endif - using var reader = new System.IO.StreamReader(stream); + using var reader = new System.IO.StreamReader(stream); - // 4GB file test, shouldn't load more than 10000 chars into a memory. - const int length = 10000; - var buffer = ArrayPool.Shared.Rent(length); - try - { - var charsRead = await reader.ReadAsync(buffer, 0, length); - resultText += new string(buffer, 0, charsRead); - } - finally - { - ArrayPool.Shared.Return(buffer); - } + // 4GB file test, shouldn't load more than 10000 chars into a memory. + const int length = 10000; + var buffer = ArrayPool.Shared.Rent(length); + try + { + var charsRead = await reader.ReadAsync(buffer, 0, length); + resultText += new string(buffer, 0, charsRead); + } + finally + { + ArrayPool.Shared.Return(buffer); } } diff --git a/src/Android/Avalonia.Android/Platform/Storage/AndroidStorageItem.cs b/src/Android/Avalonia.Android/Platform/Storage/AndroidStorageItem.cs index 9838bb06c8..9d6dd46d0e 100644 --- a/src/Android/Avalonia.Android/Platform/Storage/AndroidStorageItem.cs +++ b/src/Android/Avalonia.Android/Platform/Storage/AndroidStorageItem.cs @@ -177,11 +177,7 @@ internal sealed class AndroidStorageFile : AndroidStorageItem, IStorageBookmarkF public AndroidStorageFile(Activity activity, AndroidUri uri) : base(activity, uri, false) { } - - public bool CanOpenRead => true; - - public bool CanOpenWrite => true; - + public Task OpenReadAsync() => Task.FromResult(OpenContentStream(Activity, Uri, false) ?? throw new InvalidOperationException("Failed to open content stream")); diff --git a/src/Avalonia.Base/Platform/Storage/FileIO/BclStorageFile.cs b/src/Avalonia.Base/Platform/Storage/FileIO/BclStorageFile.cs index a4005d4f5f..5bf9ff9d9a 100644 --- a/src/Avalonia.Base/Platform/Storage/FileIO/BclStorageFile.cs +++ b/src/Avalonia.Base/Platform/Storage/FileIO/BclStorageFile.cs @@ -18,11 +18,7 @@ internal class BclStorageFile : IStorageBookmarkFile } public FileInfo FileInfo { get; } - - public bool CanOpenRead => true; - - public bool CanOpenWrite => true; - + public string Name => FileInfo.Name; public virtual bool CanBookmark => true; diff --git a/src/Avalonia.Base/Platform/Storage/IStorageFile.cs b/src/Avalonia.Base/Platform/Storage/IStorageFile.cs index 4aa84e3ec4..2a0ce15279 100644 --- a/src/Avalonia.Base/Platform/Storage/IStorageFile.cs +++ b/src/Avalonia.Base/Platform/Storage/IStorageFile.cs @@ -10,22 +10,12 @@ namespace Avalonia.Platform.Storage; [NotClientImplementable] public interface IStorageFile : IStorageItem { - /// - /// Returns true, if file is readable. - /// - bool CanOpenRead { get; } - /// /// Opens a stream for read access. /// /// Task OpenReadAsync(); - - /// - /// Returns true, if file is writeable. - /// - bool CanOpenWrite { get; } - + /// /// Opens stream for writing to the file. /// diff --git a/src/Avalonia.Diagnostics/Diagnostics/Screenshots/FilePickerHandler.cs b/src/Avalonia.Diagnostics/Diagnostics/Screenshots/FilePickerHandler.cs index a7d279741e..548d177643 100644 --- a/src/Avalonia.Diagnostics/Diagnostics/Screenshots/FilePickerHandler.cs +++ b/src/Avalonia.Diagnostics/Diagnostics/Screenshots/FilePickerHandler.cs @@ -68,10 +68,6 @@ namespace Avalonia.Diagnostics.Screenshots { return null; } - if (!result.CanOpenWrite) - { - throw new InvalidOperationException("Read-only file was selected."); - } return await result.OpenWriteAsync(); } diff --git a/src/Browser/Avalonia.Browser/Storage/BrowserStorageProvider.cs b/src/Browser/Avalonia.Browser/Storage/BrowserStorageProvider.cs index 7e8e2e0990..7189ae4111 100644 --- a/src/Browser/Avalonia.Browser/Storage/BrowserStorageProvider.cs +++ b/src/Browser/Avalonia.Browser/Storage/BrowserStorageProvider.cs @@ -216,7 +216,6 @@ internal class JSStorageFile : JSStorageItem, IStorageBookmarkFile { } - public bool CanOpenRead => true; public async Task OpenReadAsync() { try @@ -230,7 +229,6 @@ internal class JSStorageFile : JSStorageItem, IStorageBookmarkFile } } - public bool CanOpenWrite => true; public async Task OpenWriteAsync() { try diff --git a/src/iOS/Avalonia.iOS/Storage/IOSStorageItem.cs b/src/iOS/Avalonia.iOS/Storage/IOSStorageItem.cs index ef0e2467dc..6fa65f1265 100644 --- a/src/iOS/Avalonia.iOS/Storage/IOSStorageItem.cs +++ b/src/iOS/Avalonia.iOS/Storage/IOSStorageItem.cs @@ -94,11 +94,7 @@ internal sealed class IOSStorageFile : IOSStorageItem, IStorageBookmarkFile public IOSStorageFile(NSUrl url) : base(url) { } - - public bool CanOpenRead => true; - - public bool CanOpenWrite => true; - + public Task OpenReadAsync() { return Task.FromResult(new IOSSecurityScopedStream(Url, FileAccess.Read)); From d1c329e819f958a64e4bd7c069ea5e3e3c5a04bb Mon Sep 17 00:00:00 2001 From: Max Katz Date: Tue, 14 Feb 2023 10:49:35 -0500 Subject: [PATCH 28/49] Pass file extensions to the browser backend in the file picker --- samples/ControlCatalog/Pages/DialogsPage.xaml.cs | 8 +++++++- .../Platform/Storage/FilePickerFileType.cs | 16 ++++++++++++++-- .../Avalonia.Browser/Interop/StorageHelper.cs | 2 +- .../Storage/BrowserStorageProvider.cs | 2 +- .../webapp/modules/storage/storageProvider.ts | 4 ++-- .../Avalonia.iOS/Storage/IOSStorageProvider.cs | 4 ++++ 6 files changed, 29 insertions(+), 7 deletions(-) diff --git a/samples/ControlCatalog/Pages/DialogsPage.xaml.cs b/samples/ControlCatalog/Pages/DialogsPage.xaml.cs index 5f116f95b6..587b34c9fe 100644 --- a/samples/ControlCatalog/Pages/DialogsPage.xaml.cs +++ b/samples/ControlCatalog/Pages/DialogsPage.xaml.cs @@ -82,7 +82,13 @@ namespace ControlCatalog.Pages return new List { FilePickerFileTypes.All, - FilePickerFileTypes.TextPlain + FilePickerFileTypes.TextPlain, + new("Binary Log") + { + Patterns = new[] { "*.binlog", "*.buildlog" }, + MimeTypes = new[] { "application/binlog", "application/buildlog" }, + AppleUniformTypeIdentifiers = new []{ "public.data" } + } }; } diff --git a/src/Avalonia.Base/Platform/Storage/FilePickerFileType.cs b/src/Avalonia.Base/Platform/Storage/FilePickerFileType.cs index f9c7f9685d..7b0446e224 100644 --- a/src/Avalonia.Base/Platform/Storage/FilePickerFileType.cs +++ b/src/Avalonia.Base/Platform/Storage/FilePickerFileType.cs @@ -1,4 +1,6 @@ using System.Collections.Generic; +using System.IO; +using System.Linq; namespace Avalonia.Platform.Storage; @@ -21,7 +23,7 @@ public sealed class FilePickerFileType /// List of extensions in GLOB format. I.e. "*.png" or "*.*". /// /// - /// Used on Windows and Linux systems. + /// Used on Windows, Linux and Browser platforms. /// public IReadOnlyList? Patterns { get; set; } @@ -29,7 +31,7 @@ public sealed class FilePickerFileType /// List of extensions in MIME format. /// /// - /// Used on Android, Browser and Linux systems. + /// Used on Android, Linux and Browser platforms. /// public IReadOnlyList? MimeTypes { get; set; } @@ -41,4 +43,14 @@ public sealed class FilePickerFileType /// See https://developer.apple.com/documentation/uniformtypeidentifiers/system_declared_uniform_type_identifiers. /// public IReadOnlyList? AppleUniformTypeIdentifiers { get; set; } + + internal IReadOnlyList? TryGetExtensions() + { + // Converts random glob pattern to a simple extension name. + // GetExtension should be sufficient here. + // Only exception is "*.*proj" patterns that should be filtered as well. + return Patterns?.Select(Path.GetExtension) + .Where(e => !string.IsNullOrEmpty(e) && !e.Contains('*') && e.StartsWith(".")) + .ToArray()!; + } } diff --git a/src/Browser/Avalonia.Browser/Interop/StorageHelper.cs b/src/Browser/Avalonia.Browser/Interop/StorageHelper.cs index a4d7bcdb87..902dd03885 100644 --- a/src/Browser/Avalonia.Browser/Interop/StorageHelper.cs +++ b/src/Browser/Avalonia.Browser/Interop/StorageHelper.cs @@ -54,5 +54,5 @@ internal static partial class StorageHelper public static partial JSObject[] ItemsArray(JSObject item); [JSImport("StorageProvider.createAcceptType", AvaloniaModule.StorageModuleName)] - public static partial JSObject CreateAcceptType(string description, string[] mimeTypes); + public static partial JSObject CreateAcceptType(string description, string[] mimeTypes, string[]? extensions); } diff --git a/src/Browser/Avalonia.Browser/Storage/BrowserStorageProvider.cs b/src/Browser/Avalonia.Browser/Storage/BrowserStorageProvider.cs index 7189ae4111..08d7664719 100644 --- a/src/Browser/Avalonia.Browser/Storage/BrowserStorageProvider.cs +++ b/src/Browser/Avalonia.Browser/Storage/BrowserStorageProvider.cs @@ -147,7 +147,7 @@ internal class BrowserStorageProvider : IStorageProvider { var types = input? .Where(t => t.MimeTypes?.Any() == true && t != FilePickerFileTypes.All) - .Select(t => StorageHelper.CreateAcceptType(t.Name, t.MimeTypes!.ToArray())) + .Select(t => StorageHelper.CreateAcceptType(t.Name, t.MimeTypes!.ToArray(), t.TryGetExtensions()?.ToArray())) .ToArray(); if (types?.Length == 0) { diff --git a/src/Browser/Avalonia.Browser/webapp/modules/storage/storageProvider.ts b/src/Browser/Avalonia.Browser/webapp/modules/storage/storageProvider.ts index e621a1ed30..8259510ed2 100644 --- a/src/Browser/Avalonia.Browser/webapp/modules/storage/storageProvider.ts +++ b/src/Browser/Avalonia.Browser/webapp/modules/storage/storageProvider.ts @@ -62,9 +62,9 @@ export class StorageProvider { } } - public static createAcceptType(description: string, mimeTypes: string[]): FilePickerAcceptType { + public static createAcceptType(description: string, mimeTypes: string[], extensions: string[] | undefined): FilePickerAcceptType { const accept: Record = {}; - mimeTypes.forEach(a => { accept[a] = []; }); + mimeTypes.forEach(a => { accept[a] = extensions ?? []; }); return { description, accept }; } } diff --git a/src/iOS/Avalonia.iOS/Storage/IOSStorageProvider.cs b/src/iOS/Avalonia.iOS/Storage/IOSStorageProvider.cs index 376e988e74..ae2bf2a99d 100644 --- a/src/iOS/Avalonia.iOS/Storage/IOSStorageProvider.cs +++ b/src/iOS/Avalonia.iOS/Storage/IOSStorageProvider.cs @@ -43,6 +43,10 @@ internal class IOSStorageProvider : IStorageProvider { return f.AppleUniformTypeIdentifiers.Select(id => UTType.CreateFromIdentifier(id)); } + if (f.TryGetExtensions() is { } extensions && extensions.Any()) + { + return extensions.Select(id => UTType.CreateFromExtension(id.TrimStart('.'))); + } if (f.MimeTypes?.Any() == true) { return f.MimeTypes.Select(id => UTType.CreateFromMimeType(id)); From bd1928efacf84333c5f832cc88b0a1db2c120b1e Mon Sep 17 00:00:00 2001 From: Max Katz Date: Tue, 14 Feb 2023 14:51:33 -0500 Subject: [PATCH 29/49] Refactor Browser and Blazor startup code --- .../App.razor.cs | 11 +-- .../ControlCatalog.Browser.Blazor/Program.cs | 12 ++- samples/ControlCatalog.Browser/Program.cs | 20 ++++- samples/ControlCatalog.Browser/main.js | 3 - .../Platform/DefaultPlatformSettings.cs | 2 +- .../Avalonia.Browser.Blazor/AvaloniaView.cs | 14 +++- .../BlazorSingleViewLifetime.cs | 37 ++++----- src/Browser/Avalonia.Browser/AvaloniaView.cs | 5 +- .../Avalonia.Browser/BrowserAppBuilder.cs | 83 +++++++++++++++++++ .../BrowserPlatformSettings.cs | 40 ++++++--- .../BrowserSingleViewLifetime.cs | 51 +++++------- .../Interop/AvaloniaModule.cs | 4 +- .../webapp/modules/avalonia.ts | 18 +--- 13 files changed, 196 insertions(+), 104 deletions(-) create mode 100644 src/Browser/Avalonia.Browser/BrowserAppBuilder.cs diff --git a/samples/ControlCatalog.Browser.Blazor/App.razor.cs b/samples/ControlCatalog.Browser.Blazor/App.razor.cs index f38db2b055..c331625664 100644 --- a/samples/ControlCatalog.Browser.Blazor/App.razor.cs +++ b/samples/ControlCatalog.Browser.Blazor/App.razor.cs @@ -1,3 +1,5 @@ +using System; +using System.Threading.Tasks; using Avalonia; using Avalonia.Browser.Blazor; @@ -5,13 +7,4 @@ namespace ControlCatalog.Browser.Blazor; public partial class App { - protected override void OnParametersSet() - { - AppBuilder.Configure() - .UseBlazor() - // .With(new SkiaOptions { CustomGpuFactory = null }) // uncomment to disable GPU/GL rendering - .SetupWithSingleViewLifetime(); - - base.OnParametersSet(); - } } diff --git a/samples/ControlCatalog.Browser.Blazor/Program.cs b/samples/ControlCatalog.Browser.Blazor/Program.cs index eb99ca518e..500055b405 100644 --- a/samples/ControlCatalog.Browser.Blazor/Program.cs +++ b/samples/ControlCatalog.Browser.Blazor/Program.cs @@ -1,6 +1,8 @@ using System; using System.Net.Http; using System.Threading.Tasks; +using Avalonia; +using Avalonia.Browser.Blazor; using Microsoft.AspNetCore.Components.WebAssembly.Hosting; using Microsoft.Extensions.DependencyInjection; using ControlCatalog.Browser.Blazor; @@ -9,9 +11,17 @@ public class Program { public static async Task Main(string[] args) { - await CreateHostBuilder(args).Build().RunAsync(); + var host = CreateHostBuilder(args).Build(); + await StartAvaloniaApp(); + await host.RunAsync(); } + public static async Task StartAvaloniaApp() + { + await AppBuilder.Configure() + .StartBlazorApp(); + } + public static WebAssemblyHostBuilder CreateHostBuilder(string[] args) { var builder = WebAssemblyHostBuilder.CreateDefault(args); diff --git a/samples/ControlCatalog.Browser/Program.cs b/samples/ControlCatalog.Browser/Program.cs index 53b7c60a6f..4a4d8c7bb8 100644 --- a/samples/ControlCatalog.Browser/Program.cs +++ b/samples/ControlCatalog.Browser/Program.cs @@ -1,6 +1,8 @@ using System.Runtime.Versioning; +using System.Threading.Tasks; using Avalonia; using Avalonia.Browser; +using Avalonia.Controls; using ControlCatalog; using ControlCatalog.Browser; @@ -8,15 +10,27 @@ using ControlCatalog.Browser; internal partial class Program { - private static void Main(string[] args) + public static async Task Main(string[] args) { - BuildAvaloniaApp() + await BuildAvaloniaApp() .AfterSetup(_ => { ControlCatalog.Pages.EmbedSample.Implementation = new EmbedSampleWeb(); - }).SetupBrowserApp("out"); + }) + .StartBrowserApp("out"); } + // Example without a ISingleViewApplicationLifetime + // private static AvaloniaView _avaloniaView; + // public static async Task Main(string[] args) + // { + // await BuildAvaloniaApp() + // .SetupBrowserApp(); + // + // _avaloniaView = new AvaloniaView("out"); + // _avaloniaView.Content = new TextBlock { Text = "Hello world" }; + // } + public static AppBuilder BuildAvaloniaApp() => AppBuilder.Configure(); } diff --git a/samples/ControlCatalog.Browser/main.js b/samples/ControlCatalog.Browser/main.js index 87f8a4f943..9d90db8bd2 100644 --- a/samples/ControlCatalog.Browser/main.js +++ b/samples/ControlCatalog.Browser/main.js @@ -2,7 +2,6 @@ // The .NET Foundation licenses this file to you under the MIT license. import { dotnet } from './dotnet.js' -import { registerAvaloniaModule } from './avalonia.js'; const is_browser = typeof window != "undefined"; if (!is_browser) throw new Error(`Expected to be running in a browser`); @@ -12,8 +11,6 @@ const dotnetRuntime = await dotnet .withApplicationArgumentsFromQuery() .create(); -await registerAvaloniaModule(dotnetRuntime); - const config = dotnetRuntime.getConfig(); await dotnetRuntime.runMainAndExit(config.mainAssemblyName, ["dotnet", "is", "great!"]); diff --git a/src/Avalonia.Base/Platform/DefaultPlatformSettings.cs b/src/Avalonia.Base/Platform/DefaultPlatformSettings.cs index b5e7298b7e..08fcdb50aa 100644 --- a/src/Avalonia.Base/Platform/DefaultPlatformSettings.cs +++ b/src/Avalonia.Base/Platform/DefaultPlatformSettings.cs @@ -37,7 +37,7 @@ namespace Avalonia.Platform }; } - public event EventHandler? ColorValuesChanged; + public virtual event EventHandler? ColorValuesChanged; protected void OnColorValuesChanged(PlatformColorValues colorValues) { diff --git a/src/Browser/Avalonia.Browser.Blazor/AvaloniaView.cs b/src/Browser/Avalonia.Browser.Blazor/AvaloniaView.cs index 68efea31d6..1fc87fed2f 100644 --- a/src/Browser/Avalonia.Browser.Blazor/AvaloniaView.cs +++ b/src/Browser/Avalonia.Browser.Blazor/AvaloniaView.cs @@ -30,12 +30,10 @@ public class AvaloniaView : ComponentBase builder.CloseElement(); } - protected override async Task OnInitializedAsync() + protected override void OnAfterRender(bool firstRender) { - if (OperatingSystem.IsBrowser()) + if (firstRender) { - await AvaloniaModule.ImportMain(); - _browserView = new Browser.AvaloniaView(_containerId); if (Application.Current?.ApplicationLifetime is ISingleViewApplicationLifetime lifetime) { @@ -43,4 +41,12 @@ public class AvaloniaView : ComponentBase } } } + + protected override void OnInitialized() + { + if (!OperatingSystem.IsBrowser()) + { + throw new NotSupportedException("Avalonia doesn't support server-side Blazor"); + } + } } diff --git a/src/Browser/Avalonia.Browser.Blazor/BlazorSingleViewLifetime.cs b/src/Browser/Avalonia.Browser.Blazor/BlazorSingleViewLifetime.cs index a7bb5a62df..1f62690aff 100644 --- a/src/Browser/Avalonia.Browser.Blazor/BlazorSingleViewLifetime.cs +++ b/src/Browser/Avalonia.Browser.Blazor/BlazorSingleViewLifetime.cs @@ -1,33 +1,28 @@ -using System.Runtime.Versioning; - +using System; +using System.Runtime.Versioning; +using System.Threading.Tasks; +using Avalonia; +using Avalonia.Browser.Interop; using Avalonia.Controls; using Avalonia.Controls.ApplicationLifetimes; namespace Avalonia.Browser.Blazor; -public static class WebAppBuilder +public static class BlazorAppBuilder { - public static AppBuilder SetupWithSingleViewLifetime( - this AppBuilder builder) + /// + /// Configures blazor backend, loads avalonia javascript modules and creates a single view lifetime. + /// + /// Application builder. + /// Browser backend specific options. + public static async Task StartBlazorApp(this AppBuilder builder, BrowserPlatformOptions? options = null) { - return builder.SetupWithLifetime(new BlazorSingleViewLifetime()); - } + options ??= new BrowserPlatformOptions(); + options.FrameworkAssetPathResolver ??= filePath => $"/_content/Avalonia.Browser.Blazor/{filePath}"; - public static AppBuilder UseBlazor(this AppBuilder builder) - { - return builder - .UseBrowser() - .With(new BrowserPlatformOptions - { - FrameworkAssetPathResolver = new(filePath => $"/_content/Avalonia.Browser.Blazor/{filePath}") - }); - } + builder = await BrowserAppBuilder.PreSetupBrowser(builder, options); - public static AppBuilder Configure() - where TApp : Application, new() - { - return AppBuilder.Configure() - .UseBlazor(); + builder.SetupWithLifetime(new BlazorSingleViewLifetime()); } internal class BlazorSingleViewLifetime : ISingleViewApplicationLifetime diff --git a/src/Browser/Avalonia.Browser/AvaloniaView.cs b/src/Browser/Avalonia.Browser/AvaloniaView.cs index 3f4aa0d0ba..775100d76b 100644 --- a/src/Browser/Avalonia.Browser/AvaloniaView.cs +++ b/src/Browser/Avalonia.Browser/AvaloniaView.cs @@ -20,7 +20,7 @@ using static System.Runtime.CompilerServices.RuntimeHelpers; namespace Avalonia.Browser { - public partial class AvaloniaView : ITextInputMethodImpl + public class AvaloniaView : ITextInputMethodImpl { private static readonly PooledList s_intermediatePointsPooledList = new(ClearMode.Never); private readonly BrowserTopLevelImpl _topLevelImpl; @@ -43,8 +43,9 @@ namespace Avalonia.Browser private bool _useGL; private ITextInputMethodClient? _client; + /// ID of the html element where avalonia content should be rendered. public AvaloniaView(string divId) - : this(DomHelper.GetElementById(divId) ?? throw new Exception($"Element with id {divId} was not found in the html document.")) + : this(DomHelper.GetElementById(divId) ?? throw new Exception($"Element with id '{divId}' was not found in the html document.")) { } diff --git a/src/Browser/Avalonia.Browser/BrowserAppBuilder.cs b/src/Browser/Avalonia.Browser/BrowserAppBuilder.cs new file mode 100644 index 0000000000..866c8ceca4 --- /dev/null +++ b/src/Browser/Avalonia.Browser/BrowserAppBuilder.cs @@ -0,0 +1,83 @@ +using System; +using System.Threading.Tasks; +using Avalonia.Browser.Interop; + +namespace Avalonia.Browser; + +public class BrowserPlatformOptions +{ + /// + /// Defines paths where avalonia modules and service locator should be resolved. + /// If null, default path resolved depending on the backend (browser or blazor) is used. + /// + public Func? FrameworkAssetPathResolver { get; set; } +} + +public static class BrowserAppBuilder +{ + /// + /// Configures browser backend, loads avalonia javascript modules and creates a single view lifetime from the passed parameter. + /// + /// Application builder. + /// ID of the html element where avalonia content should be rendered. + /// Browser backend specific options. + public static async Task StartBrowserApp(this AppBuilder builder, string mainDivId, BrowserPlatformOptions? options = null) + { + if (mainDivId is null) + { + throw new ArgumentNullException(nameof(mainDivId)); + } + + builder = await PreSetupBrowser(builder, options); + + var lifetime = new BrowserSingleViewLifetime(); + builder + .AfterSetup(_ => + { + lifetime.View = new AvaloniaView(mainDivId); + }) + .SetupWithLifetime(lifetime); + } + + /// + /// Loads avalonia javascript modules and configures browser backend. + /// + /// Application builder. + /// Browser backend specific options. + /// + /// This method doesn't creates any avalonia views to be rendered. To do so create an object. + /// Alternatively, you can call method instead of . + /// + public static async Task SetupBrowserApp(this AppBuilder builder, BrowserPlatformOptions? options = null) + { + builder = await PreSetupBrowser(builder, options); + + builder + .SetupWithoutStarting(); + } + + internal static async Task PreSetupBrowser(AppBuilder builder, BrowserPlatformOptions? options) + { + options ??= new BrowserPlatformOptions(); + options.FrameworkAssetPathResolver ??= fileName => $"./{fileName}"; + + AvaloniaLocator.CurrentMutable.Bind().ToConstant(options); + + await AvaloniaModule.ImportMain(); + + if (builder.WindowingSubsystemInitializer is null) + { + builder = builder.UseBrowser(); + } + + return builder; + } + + public static AppBuilder UseBrowser( + this AppBuilder builder) + { + return builder + .UseWindowingSubsystem(BrowserWindowingPlatform.Register) + .UseSkia(); + } +} diff --git a/src/Browser/Avalonia.Browser/BrowserPlatformSettings.cs b/src/Browser/Avalonia.Browser/BrowserPlatformSettings.cs index 6084c5c7de..fa647d31b7 100644 --- a/src/Browser/Avalonia.Browser/BrowserPlatformSettings.cs +++ b/src/Browser/Avalonia.Browser/BrowserPlatformSettings.cs @@ -1,4 +1,5 @@ -using Avalonia.Browser.Interop; +using System; +using Avalonia.Browser.Interop; using Avalonia.Platform; namespace Avalonia.Browser; @@ -7,25 +8,44 @@ internal class BrowserPlatformSettings : DefaultPlatformSettings { private bool _isDarkMode; private bool _isHighContrast; - - public BrowserPlatformSettings() + private bool _isInitialized; + + public override event EventHandler? ColorValuesChanged { - var obj = DomHelper.ObserveDarkMode((isDarkMode, isHighContrast) => + add { - _isDarkMode = isDarkMode; - _isHighContrast = isHighContrast; - OnColorValuesChanged(GetColorValues()); - }); - _isDarkMode = obj.GetPropertyAsBoolean("isDarkMode"); - _isHighContrast = obj.GetPropertyAsBoolean("isHighContrast"); + EnsureBackend(); + base.ColorValuesChanged += value; + } + remove => base.ColorValuesChanged -= value; } public override PlatformColorValues GetColorValues() { + EnsureBackend(); + return base.GetColorValues() with { ThemeVariant = _isDarkMode ? PlatformThemeVariant.Dark : PlatformThemeVariant.Light, ContrastPreference = _isHighContrast ? ColorContrastPreference.High : ColorContrastPreference.NoPreference }; } + + private void EnsureBackend() + { + if (!_isInitialized) + { + // WASM module has async nature of initialization. We can't native code right away during components registration. + _isInitialized = true; + + var obj = DomHelper.ObserveDarkMode((isDarkMode, isHighContrast) => + { + _isDarkMode = isDarkMode; + _isHighContrast = isHighContrast; + OnColorValuesChanged(GetColorValues()); + }); + _isDarkMode = obj.GetPropertyAsBoolean("isDarkMode"); + _isHighContrast = obj.GetPropertyAsBoolean("isHighContrast"); + } + } } diff --git a/src/Browser/Avalonia.Browser/BrowserSingleViewLifetime.cs b/src/Browser/Avalonia.Browser/BrowserSingleViewLifetime.cs index add69760ee..6fa79f6f54 100644 --- a/src/Browser/Avalonia.Browser/BrowserSingleViewLifetime.cs +++ b/src/Browser/Avalonia.Browser/BrowserSingleViewLifetime.cs @@ -1,47 +1,36 @@ using System; +using System.Diagnostics.CodeAnalysis; using Avalonia.Controls; using Avalonia.Controls.ApplicationLifetimes; using System.Runtime.Versioning; +using Avalonia.Browser; -namespace Avalonia.Browser; +namespace Avalonia; -public class BrowserSingleViewLifetime : ISingleViewApplicationLifetime +internal class BrowserSingleViewLifetime : ISingleViewApplicationLifetime { public AvaloniaView? View; public Control? MainView { - get => View!.Content; - set => View!.Content = value; - } -} - -public class BrowserPlatformOptions -{ - public Func FrameworkAssetPathResolver { get; set; } = new(fileName => $"./{fileName}"); -} - -public static class WebAppBuilder -{ - public static AppBuilder SetupBrowserApp( - this AppBuilder builder, string mainDivId) - { - var lifetime = new BrowserSingleViewLifetime(); - - return builder - .UseBrowser() - .AfterSetup(b => - { - lifetime.View = new AvaloniaView(mainDivId); - }) - .SetupWithLifetime(lifetime); + get + { + EnsureView(); + return View.Content; + } + set + { + EnsureView(); + View.Content = value; + } } - public static AppBuilder UseBrowser( - this AppBuilder builder) + [MemberNotNull(nameof(View))] + private void EnsureView() { - return builder - .UseWindowingSubsystem(BrowserWindowingPlatform.Register) - .UseSkia(); + if (View is null) + { + throw new InvalidOperationException("Browser lifetime was not initialized. Make sure AppBuilder.StartBrowserApp was called."); + } } } diff --git a/src/Browser/Avalonia.Browser/Interop/AvaloniaModule.cs b/src/Browser/Avalonia.Browser/Interop/AvaloniaModule.cs index b283fbaa56..f1936a8d97 100644 --- a/src/Browser/Avalonia.Browser/Interop/AvaloniaModule.cs +++ b/src/Browser/Avalonia.Browser/Interop/AvaloniaModule.cs @@ -11,13 +11,13 @@ internal static partial class AvaloniaModule public static Task ImportMain() { var options = AvaloniaLocator.Current.GetService() ?? new BrowserPlatformOptions(); - return JSHost.ImportAsync(MainModuleName, options.FrameworkAssetPathResolver("avalonia.js")); + return JSHost.ImportAsync(MainModuleName, options.FrameworkAssetPathResolver!("avalonia.js")); } public static Task ImportStorage() { var options = AvaloniaLocator.Current.GetService() ?? new BrowserPlatformOptions(); - return JSHost.ImportAsync(StorageModuleName, options.FrameworkAssetPathResolver("storage.js")); + return JSHost.ImportAsync(StorageModuleName, options.FrameworkAssetPathResolver!("storage.js")); } [JSImport("Caniuse.isMobile", AvaloniaModule.MainModuleName)] diff --git a/src/Browser/Avalonia.Browser/webapp/modules/avalonia.ts b/src/Browser/Avalonia.Browser/webapp/modules/avalonia.ts index ab0c85eaa2..3fb4124c96 100644 --- a/src/Browser/Avalonia.Browser/webapp/modules/avalonia.ts +++ b/src/Browser/Avalonia.Browser/webapp/modules/avalonia.ts @@ -1,4 +1,3 @@ -import { RuntimeAPI } from "../types/dotnet"; import { SizeWatcher, DpiWatcher, Canvas } from "./avalonia/canvas"; import { InputHelper } from "./avalonia/input"; import { AvaloniaDOM } from "./avalonia/dom"; @@ -7,19 +6,6 @@ import { StreamHelper } from "./avalonia/stream"; import { NativeControlHost } from "./avalonia/nativeControlHost"; import { NavigationHelper } from "./avalonia/navigationHelper"; -async function registerAvaloniaModule(api: RuntimeAPI): Promise { - api.setModuleImports("avalonia", { - Caniuse, - Canvas, - InputHelper, - SizeWatcher, - DpiWatcher, - AvaloniaDOM, - StreamHelper, - NativeControlHost, - NavigationHelper - }); -} export { Caniuse, Canvas, @@ -29,7 +15,5 @@ export { AvaloniaDOM, StreamHelper, NativeControlHost, - NavigationHelper, - - registerAvaloniaModule + NavigationHelper }; From 49a6cbd5a6c6174c9745b2a22c75a239836d9412 Mon Sep 17 00:00:00 2001 From: Max Katz Date: Tue, 14 Feb 2023 17:31:29 -0500 Subject: [PATCH 30/49] Use polyfill for open file and directory dialogs --- .../ControlCatalog/Pages/DialogsPage.xaml.cs | 2 +- src/Browser/Avalonia.Browser/AvaloniaView.cs | 5 - .../Avalonia.Browser/Interop/StorageHelper.cs | 10 +- .../Storage/BrowserStorageProvider.cs | 18 ++- .../Avalonia.Browser/webapp/.eslintrc.json | 2 +- src/Browser/Avalonia.Browser/webapp/build.js | 2 +- .../webapp/modules/avalonia/caniuse.ts | 12 +- .../webapp/modules/avalonia/stream.ts | 3 +- .../webapp/modules/storage/storageItem.ts | 50 +++++--- .../webapp/modules/storage/storageProvider.ts | 25 ++-- .../Avalonia.Browser/webapp/package-lock.json | 117 ++++++++++++++++-- .../Avalonia.Browser/webapp/package.json | 4 +- .../Avalonia.Browser/webapp/tsconfig.json | 6 +- 13 files changed, 178 insertions(+), 78 deletions(-) diff --git a/samples/ControlCatalog/Pages/DialogsPage.xaml.cs b/samples/ControlCatalog/Pages/DialogsPage.xaml.cs index 587b34c9fe..61e10729cc 100644 --- a/samples/ControlCatalog/Pages/DialogsPage.xaml.cs +++ b/samples/ControlCatalog/Pages/DialogsPage.xaml.cs @@ -281,7 +281,7 @@ namespace ControlCatalog.Pages { ignoreTextChanged = true; lastSelectedDirectory = folder; - currentFolderBox.Text = folder?.Path.LocalPath; + currentFolderBox.Text = folder?.Path is { IsAbsoluteUri: true } abs ? abs.LocalPath : folder?.Path?.ToString(); ignoreTextChanged = false; } async Task SetPickerResult(IReadOnlyCollection? items) diff --git a/src/Browser/Avalonia.Browser/AvaloniaView.cs b/src/Browser/Avalonia.Browser/AvaloniaView.cs index 775100d76b..3bb7260e55 100644 --- a/src/Browser/Avalonia.Browser/AvaloniaView.cs +++ b/src/Browser/Avalonia.Browser/AvaloniaView.cs @@ -381,12 +381,10 @@ namespace Avalonia.Browser { if (_useGL && (_jsGlInfo == null)) { - Console.WriteLine("nothing to render"); return; } if (_canvasSize.Width <= 0 || _canvasSize.Height <= 0 || _dpi <= 0) { - Console.WriteLine("nothing to render"); return; } @@ -459,7 +457,6 @@ namespace Avalonia.Browser void ITextInputMethodImpl.SetClient(ITextInputMethodClient? client) { - Console.WriteLine("Set Client"); if (_client != null) { _client.SurroundingTextChanged -= SurroundingTextChanged; @@ -482,8 +479,6 @@ namespace Avalonia.Browser var surroundingText = _client.SurroundingText; InputHelper.SetSurroundingText(_inputElement, surroundingText.Text, surroundingText.AnchorOffset, surroundingText.CursorOffset); - - Console.WriteLine("Shown, focused and surrounded."); } else { diff --git a/src/Browser/Avalonia.Browser/Interop/StorageHelper.cs b/src/Browser/Avalonia.Browser/Interop/StorageHelper.cs index 902dd03885..11beba6f2c 100644 --- a/src/Browser/Avalonia.Browser/Interop/StorageHelper.cs +++ b/src/Browser/Avalonia.Browser/Interop/StorageHelper.cs @@ -5,14 +5,8 @@ namespace Avalonia.Browser.Interop; internal static partial class StorageHelper { - [JSImport("Caniuse.canShowOpenFilePicker", AvaloniaModule.MainModuleName)] - public static partial bool CanShowOpenFilePicker(); - - [JSImport("Caniuse.canShowSaveFilePicker", AvaloniaModule.MainModuleName)] - public static partial bool CanShowSaveFilePicker(); - - [JSImport("Caniuse.canShowDirectoryPicker", AvaloniaModule.MainModuleName)] - public static partial bool CanShowDirectoryPicker(); + [JSImport("Caniuse.hasNativeFilePicker", AvaloniaModule.MainModuleName)] + public static partial bool HasNativeFilePicker(); [JSImport("StorageProvider.selectFolderDialog", AvaloniaModule.StorageModuleName)] public static partial Task SelectFolderDialog(JSObject? startIn); diff --git a/src/Browser/Avalonia.Browser/Storage/BrowserStorageProvider.cs b/src/Browser/Avalonia.Browser/Storage/BrowserStorageProvider.cs index 08d7664719..7497e82384 100644 --- a/src/Browser/Avalonia.Browser/Storage/BrowserStorageProvider.cs +++ b/src/Browser/Avalonia.Browser/Storage/BrowserStorageProvider.cs @@ -20,9 +20,9 @@ internal class BrowserStorageProvider : IStorageProvider private readonly Lazy _lazyModule = new(() => AvaloniaModule.ImportStorage()); - public bool CanOpen => StorageHelper.CanShowOpenFilePicker(); - public bool CanSave => StorageHelper.CanShowSaveFilePicker(); - public bool CanPickFolder => StorageHelper.CanShowDirectoryPicker(); + public bool CanOpen => true; + public bool CanSave => StorageHelper.HasNativeFilePicker(); + public bool CanPickFolder => true; public async Task> OpenFilePickerAsync(FilePickerOpenOptions options) { @@ -186,10 +186,15 @@ internal abstract class JSStorageItem : IStorageBookmarkItem dateModified: lastModified > 0 ? DateTimeOffset.FromUnixTimeMilliseconds(lastModified.Value) : null); } - public bool CanBookmark => true; + public bool CanBookmark => StorageHelper.HasNativeFilePicker(); public Task SaveBookmarkAsync() { + if (!CanBookmark) + { + return Task.FromResult(null); + } + return StorageHelper.SaveBookmark(FileHandle); } @@ -200,6 +205,11 @@ internal abstract class JSStorageItem : IStorageBookmarkItem public Task ReleaseBookmarkAsync() { + if (!CanBookmark) + { + return Task.CompletedTask; + } + return StorageHelper.DeleteBookmark(FileHandle); } diff --git a/src/Browser/Avalonia.Browser/webapp/.eslintrc.json b/src/Browser/Avalonia.Browser/webapp/.eslintrc.json index 4b7e24987f..f4fb8e37bf 100644 --- a/src/Browser/Avalonia.Browser/webapp/.eslintrc.json +++ b/src/Browser/Avalonia.Browser/webapp/.eslintrc.json @@ -43,5 +43,5 @@ } ] }, - "ignorePatterns": ["types/*"] + "ignorePatterns": ["types/*","node_modules/*"] } diff --git a/src/Browser/Avalonia.Browser/webapp/build.js b/src/Browser/Avalonia.Browser/webapp/build.js index 81f863cac7..c1cbc84709 100644 --- a/src/Browser/Avalonia.Browser/webapp/build.js +++ b/src/Browser/Avalonia.Browser/webapp/build.js @@ -7,7 +7,7 @@ require("esbuild").build({ bundle: true, minify: true, format: "esm", - target: "es2016", + target: "es2018", platform: "browser", sourcemap: "linked", loader: { ".ts": "ts" } diff --git a/src/Browser/Avalonia.Browser/webapp/modules/avalonia/caniuse.ts b/src/Browser/Avalonia.Browser/webapp/modules/avalonia/caniuse.ts index e019f92113..8fdc3a5c01 100644 --- a/src/Browser/Avalonia.Browser/webapp/modules/avalonia/caniuse.ts +++ b/src/Browser/Avalonia.Browser/webapp/modules/avalonia/caniuse.ts @@ -1,14 +1,6 @@ export class Caniuse { - public static canShowOpenFilePicker(): boolean { - return typeof globalThis.showOpenFilePicker !== "undefined"; - } - - public static canShowSaveFilePicker(): boolean { - return typeof globalThis.showSaveFilePicker !== "undefined"; - } - - public static canShowDirectoryPicker(): boolean { - return typeof globalThis.showDirectoryPicker !== "undefined"; + public static hasNativeFilePicker(): boolean { + return "showSaveFilePicker" in globalThis; } public static isMobile(): boolean { diff --git a/src/Browser/Avalonia.Browser/webapp/modules/avalonia/stream.ts b/src/Browser/Avalonia.Browser/webapp/modules/avalonia/stream.ts index 1f2c181edc..7c7769ea36 100644 --- a/src/Browser/Avalonia.Browser/webapp/modules/avalonia/stream.ts +++ b/src/Browser/Avalonia.Browser/webapp/modules/avalonia/stream.ts @@ -1,3 +1,4 @@ +import FileSystemWritableFileStream from "native-file-system-adapter/types/src/FileSystemWritableFileStream"; import { IMemoryView } from "../../types/dotnet"; export class StreamHelper { @@ -17,7 +18,7 @@ export class StreamHelper { const array = new Uint8Array(span.byteLength); span.copyTo(array); - const data: WriteParams = { + const data = { type: "write", data: array }; diff --git a/src/Browser/Avalonia.Browser/webapp/modules/storage/storageItem.ts b/src/Browser/Avalonia.Browser/webapp/modules/storage/storageItem.ts index c6e5254329..8f47e61100 100644 --- a/src/Browser/Avalonia.Browser/webapp/modules/storage/storageItem.ts +++ b/src/Browser/Avalonia.Browser/webapp/modules/storage/storageItem.ts @@ -1,8 +1,10 @@ import { avaloniaDb, fileBookmarksStore } from "./indexedDb"; +import { FileSystemFileHandle, FileSystemDirectoryHandle, FileSystemWritableFileStream } from "native-file-system-adapter"; +import { Caniuse } from "../avalonia"; export class StorageItem { constructor( - public handle?: FileSystemHandle, + public handle?: FileSystemFileHandle | FileSystemDirectoryHandle, private readonly bookmarkId?: string, public wellKnownType?: WellKnownDirectory ) { @@ -27,39 +29,44 @@ export class StorageItem { } public static async openRead(item: StorageItem): Promise { - if (!(item.handle instanceof FileSystemFileHandle)) { + if (!item.handle || item.kind !== "file") { throw new Error("StorageItem is not a file"); } await item.verityPermissions("read"); - const file = await item.handle.getFile(); + const file = await (item.handle as FileSystemFileHandle).getFile(); return file; } public static async openWrite(item: StorageItem): Promise { - if (!(item.handle instanceof FileSystemFileHandle)) { + if (!item.handle || item.kind !== "file") { throw new Error("StorageItem is not a file"); } await item.verityPermissions("readwrite"); - return await item.handle.createWritable({ keepExistingData: true }); + return await (item.handle as FileSystemFileHandle).createWritable({ keepExistingData: true }); } public static async getProperties(item: StorageItem): Promise<{ Size: number; LastModified: number; Type: string } | null> { - const file = item.handle instanceof FileSystemFileHandle && - await item.handle.getFile(); - - if (!file) { + // getFile can fail with an exception depending if we use polyfill with a save file dialog or not. + try { + const file = item.handle instanceof FileSystemFileHandle && + await item.handle.getFile(); + + if (!file) { + return null; + } + + return { + Size: file.size, + LastModified: file.lastModified, + Type: file.type + }; + } catch { return null; } - - return { - Size: file.size, - LastModified: file.lastModified, - Type: file.type - }; } public static async getItems(item: StorageItem): Promise { @@ -74,11 +81,16 @@ export class StorageItem { return new StorageItems(items); } - private async verityPermissions(mode: FileSystemPermissionMode): Promise { + private async verityPermissions(mode: "read" | "readwrite"): Promise { if (!this.handle) { return; } + // If we are using polyfill, let it decide permissions by itself, we can't request anything in this case. + if (!Caniuse.hasNativeFilePicker()) { + return; + } + if (await this.handle.queryPermission({ mode }) === "granted") { return; } @@ -93,7 +105,9 @@ export class StorageItem { if (item.bookmarkId) { return item.bookmarkId; } - if (!item.handle) { + + // Bookmarks are not supported with polyfill. + if (!item.handle || !Caniuse.hasNativeFilePicker()) { return null; } @@ -107,7 +121,7 @@ export class StorageItem { } public static async deleteBookmark(item: StorageItem): Promise { - if (!item.bookmarkId) { + if (!item.bookmarkId || !Caniuse.hasNativeFilePicker()) { return; } diff --git a/src/Browser/Avalonia.Browser/webapp/modules/storage/storageProvider.ts b/src/Browser/Avalonia.Browser/webapp/modules/storage/storageProvider.ts index 8259510ed2..750c38b8ea 100644 --- a/src/Browser/Avalonia.Browser/webapp/modules/storage/storageProvider.ts +++ b/src/Browser/Avalonia.Browser/webapp/modules/storage/storageProvider.ts @@ -1,14 +1,12 @@ import { avaloniaDb, fileBookmarksStore } from "./indexedDb"; import { StorageItem, StorageItems } from "./storageItem"; +import { showOpenFilePicker, showDirectoryPicker, FileSystemFileHandle } from "native-file-system-adapter"; declare global { type WellKnownDirectory = "desktop" | "documents" | "downloads" | "music" | "pictures" | "videos"; - type StartInDirectory = WellKnownDirectory | FileSystemHandle; - interface OpenFilePickerOptions { - startIn?: StartInDirectory; - } - interface SaveFilePickerOptions { - startIn?: StartInDirectory; + interface FilePickerAcceptType { + description?: string | undefined; + accept: Record; } } @@ -16,39 +14,40 @@ export class StorageProvider { public static async selectFolderDialog( startIn: StorageItem | null): Promise { // 'Picker' API doesn't accept "null" as a parameter, so it should be set to undefined. - const options: DirectoryPickerOptions = { + const options = { startIn: (startIn?.wellKnownType ?? startIn?.handle ?? undefined) }; - const handle = await window.showDirectoryPicker(options); + const handle = await showDirectoryPicker(options as any); return new StorageItem(handle); } public static async openFileDialog( startIn: StorageItem | null, multiple: boolean, types: FilePickerAcceptType[] | null, excludeAcceptAllOption: boolean): Promise { - const options: OpenFilePickerOptions = { + const options = { startIn: (startIn?.wellKnownType ?? startIn?.handle ?? undefined), multiple, excludeAcceptAllOption, types: (types ?? undefined) }; - const handles = await window.showOpenFilePicker(options); - return new StorageItems(handles.map((handle: FileSystemHandle) => new StorageItem(handle))); + const handles = await showOpenFilePicker(options); + return new StorageItems(handles.map((handle: FileSystemFileHandle) => new StorageItem(handle))); } public static async saveFileDialog( startIn: StorageItem | null, suggestedName: string | null, types: FilePickerAcceptType[] | null, excludeAcceptAllOption: boolean): Promise { - const options: SaveFilePickerOptions = { + const options = { startIn: (startIn?.wellKnownType ?? startIn?.handle ?? undefined), suggestedName: (suggestedName ?? undefined), excludeAcceptAllOption, types: (types ?? undefined) }; - const handle = await window.showSaveFilePicker(options); + // Always prefer native save file picker, as polyfill solutions are not reliable. + const handle = await (globalThis as any).showSaveFilePicker(options); return new StorageItem(handle); } diff --git a/src/Browser/Avalonia.Browser/webapp/package-lock.json b/src/Browser/Avalonia.Browser/webapp/package-lock.json index 2d875e84db..12757fd7a0 100644 --- a/src/Browser/Avalonia.Browser/webapp/package-lock.json +++ b/src/Browser/Avalonia.Browser/webapp/package-lock.json @@ -5,9 +5,11 @@ "packages": { "": { "name": "avalonia.browser", + "dependencies": { + "native-file-system-adapter": "github:jimmywarting/native-file-system-adapter#d43ad841581c2cc3ce47bbd1e8f11950ebdff027" + }, "devDependencies": { "@types/emscripten": "^1.39.6", - "@types/wicg-file-system-access": "^2020.9.5", "@typescript-eslint/eslint-plugin": "^5.38.1", "esbuild": "^0.15.7", "eslint": "^8.24.0", @@ -170,12 +172,6 @@ "integrity": "sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==", "dev": true }, - "node_modules/@types/wicg-file-system-access": { - "version": "2020.9.5", - "resolved": "https://registry.npmjs.org/@types/wicg-file-system-access/-/wicg-file-system-access-2020.9.5.tgz", - "integrity": "sha512-UYK244awtmcUYQfs7FR8710MJcefL2WvkyHMjA8yJzxd1mo0Gfn88sRZ1Bls7hiUhA2w7ne1gpJ9T5g3G0wOyA==", - "dev": true - }, "node_modules/@typescript-eslint/eslint-plugin": { "version": "5.38.1", "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-5.38.1.tgz", @@ -1573,6 +1569,29 @@ "reusify": "^1.0.4" } }, + "node_modules/fetch-blob": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/fetch-blob/-/fetch-blob-3.2.0.tgz", + "integrity": "sha512-7yAQpD2UMJzLi1Dqv7qFYnPbaPx7ZfFK6PiIxQ4PfkGPyNyl2Ugx+a/umUonmKqjhM4DnfbMvdX6otXq83soQQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/jimmywarting" + }, + { + "type": "paypal", + "url": "https://paypal.me/jimmywarting" + } + ], + "optional": true, + "dependencies": { + "node-domexception": "^1.0.0", + "web-streams-polyfill": "^3.0.3" + }, + "engines": { + "node": "^12.20 || >= 14.13" + } + }, "node_modules/file-entry-cache": { "version": "6.0.1", "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-6.0.1.tgz", @@ -2289,6 +2308,27 @@ "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", "dev": true }, + "node_modules/native-file-system-adapter": { + "version": "3.0.0", + "resolved": "git+ssh://git@github.com/jimmywarting/native-file-system-adapter.git#d43ad841581c2cc3ce47bbd1e8f11950ebdff027", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/jimmywarting" + }, + { + "type": "paypal", + "url": "https://paypal.me/jimmywarting" + } + ], + "license": "MIT", + "engines": { + "node": ">=14.8.0" + }, + "optionalDependencies": { + "fetch-blob": "^3.2.0" + } + }, "node_modules/natural-compare": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", @@ -2301,6 +2341,25 @@ "integrity": "sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ==", "dev": true }, + "node_modules/node-domexception": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/node-domexception/-/node-domexception-1.0.0.tgz", + "integrity": "sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/jimmywarting" + }, + { + "type": "github", + "url": "https://paypal.me/jimmywarting" + } + ], + "optional": true, + "engines": { + "node": ">=10.5.0" + } + }, "node_modules/normalize-package-data": { "version": "2.5.0", "resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-2.5.0.tgz", @@ -3196,6 +3255,15 @@ "spdx-expression-parse": "^3.0.0" } }, + "node_modules/web-streams-polyfill": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/web-streams-polyfill/-/web-streams-polyfill-3.2.1.tgz", + "integrity": "sha512-e0MO3wdXWKrLbL0DgGnUV7WHVuw9OUvL4hjgnPkIeEvESk74gAITi5G606JtZPp39cd8HA9VQzCIvA49LpPN5Q==", + "optional": true, + "engines": { + "node": ">= 8" + } + }, "node_modules/which": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", @@ -3366,12 +3434,6 @@ "integrity": "sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==", "dev": true }, - "@types/wicg-file-system-access": { - "version": "2020.9.5", - "resolved": "https://registry.npmjs.org/@types/wicg-file-system-access/-/wicg-file-system-access-2020.9.5.tgz", - "integrity": "sha512-UYK244awtmcUYQfs7FR8710MJcefL2WvkyHMjA8yJzxd1mo0Gfn88sRZ1Bls7hiUhA2w7ne1gpJ9T5g3G0wOyA==", - "dev": true - }, "@typescript-eslint/eslint-plugin": { "version": "5.38.1", "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-5.38.1.tgz", @@ -4275,6 +4337,16 @@ "reusify": "^1.0.4" } }, + "fetch-blob": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/fetch-blob/-/fetch-blob-3.2.0.tgz", + "integrity": "sha512-7yAQpD2UMJzLi1Dqv7qFYnPbaPx7ZfFK6PiIxQ4PfkGPyNyl2Ugx+a/umUonmKqjhM4DnfbMvdX6otXq83soQQ==", + "optional": true, + "requires": { + "node-domexception": "^1.0.0", + "web-streams-polyfill": "^3.0.3" + } + }, "file-entry-cache": { "version": "6.0.1", "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-6.0.1.tgz", @@ -4796,6 +4868,13 @@ "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", "dev": true }, + "native-file-system-adapter": { + "version": "git+ssh://git@github.com/jimmywarting/native-file-system-adapter.git#d43ad841581c2cc3ce47bbd1e8f11950ebdff027", + "from": "native-file-system-adapter@github:jimmywarting/native-file-system-adapter#d43ad841581c2cc3ce47bbd1e8f11950ebdff027", + "requires": { + "fetch-blob": "^3.2.0" + } + }, "natural-compare": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", @@ -4808,6 +4887,12 @@ "integrity": "sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ==", "dev": true }, + "node-domexception": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/node-domexception/-/node-domexception-1.0.0.tgz", + "integrity": "sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==", + "optional": true + }, "normalize-package-data": { "version": "2.5.0", "resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-2.5.0.tgz", @@ -5446,6 +5531,12 @@ "spdx-expression-parse": "^3.0.0" } }, + "web-streams-polyfill": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/web-streams-polyfill/-/web-streams-polyfill-3.2.1.tgz", + "integrity": "sha512-e0MO3wdXWKrLbL0DgGnUV7WHVuw9OUvL4hjgnPkIeEvESk74gAITi5G606JtZPp39cd8HA9VQzCIvA49LpPN5Q==", + "optional": true + }, "which": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", diff --git a/src/Browser/Avalonia.Browser/webapp/package.json b/src/Browser/Avalonia.Browser/webapp/package.json index 05a3976ccc..20dd8d4e6a 100644 --- a/src/Browser/Avalonia.Browser/webapp/package.json +++ b/src/Browser/Avalonia.Browser/webapp/package.json @@ -8,7 +8,6 @@ }, "devDependencies": { "@types/emscripten": "^1.39.6", - "@types/wicg-file-system-access": "^2020.9.5", "@typescript-eslint/eslint-plugin": "^5.38.1", "esbuild": "^0.15.7", "eslint": "^8.24.0", @@ -18,5 +17,8 @@ "eslint-plugin-promise": "^6.0.1", "npm-run-all": "^4.1.5", "typescript": "^4.8.3" + }, + "dependencies": { + "native-file-system-adapter": "github:jimmywarting/native-file-system-adapter#d43ad841581c2cc3ce47bbd1e8f11950ebdff027" } } diff --git a/src/Browser/Avalonia.Browser/webapp/tsconfig.json b/src/Browser/Avalonia.Browser/webapp/tsconfig.json index ad0e727150..1450ce4c57 100644 --- a/src/Browser/Avalonia.Browser/webapp/tsconfig.json +++ b/src/Browser/Avalonia.Browser/webapp/tsconfig.json @@ -1,14 +1,16 @@ { "compilerOptions": { - "target": "es2016", + "target": "es2018", "module": "es2020", "strict": true, "sourceMap": true, "noEmitOnError": true, + "moduleResolution": "node", + "skipLibCheck": true, "isolatedModules": true, // we need it for esbuild "lib": [ "dom", - "es2016", + "es2018", "esnext.asynciterable" ] }, From 696c5232e76f68011313466bf51c158ab00cee01 Mon Sep 17 00:00:00 2001 From: Max Katz Date: Tue, 14 Feb 2023 20:12:38 -0500 Subject: [PATCH 31/49] Rename StartBrowserApp-like methods to end with "Async" suffix --- samples/ControlCatalog.Browser.Blazor/Program.cs | 2 +- samples/ControlCatalog.Browser/Program.cs | 2 +- .../Avalonia.Browser.Blazor/BlazorSingleViewLifetime.cs | 2 +- src/Browser/Avalonia.Browser/BrowserAppBuilder.cs | 6 +++--- 4 files changed, 6 insertions(+), 6 deletions(-) diff --git a/samples/ControlCatalog.Browser.Blazor/Program.cs b/samples/ControlCatalog.Browser.Blazor/Program.cs index 500055b405..e68e9b14d9 100644 --- a/samples/ControlCatalog.Browser.Blazor/Program.cs +++ b/samples/ControlCatalog.Browser.Blazor/Program.cs @@ -19,7 +19,7 @@ public class Program public static async Task StartAvaloniaApp() { await AppBuilder.Configure() - .StartBlazorApp(); + .StartBlazorAppAsync(); } public static WebAssemblyHostBuilder CreateHostBuilder(string[] args) diff --git a/samples/ControlCatalog.Browser/Program.cs b/samples/ControlCatalog.Browser/Program.cs index 4a4d8c7bb8..e1a4500173 100644 --- a/samples/ControlCatalog.Browser/Program.cs +++ b/samples/ControlCatalog.Browser/Program.cs @@ -17,7 +17,7 @@ internal partial class Program { ControlCatalog.Pages.EmbedSample.Implementation = new EmbedSampleWeb(); }) - .StartBrowserApp("out"); + .StartBrowserAppAsync("out"); } // Example without a ISingleViewApplicationLifetime diff --git a/src/Browser/Avalonia.Browser.Blazor/BlazorSingleViewLifetime.cs b/src/Browser/Avalonia.Browser.Blazor/BlazorSingleViewLifetime.cs index 1f62690aff..7c5ee27c66 100644 --- a/src/Browser/Avalonia.Browser.Blazor/BlazorSingleViewLifetime.cs +++ b/src/Browser/Avalonia.Browser.Blazor/BlazorSingleViewLifetime.cs @@ -15,7 +15,7 @@ public static class BlazorAppBuilder /// /// Application builder. /// Browser backend specific options. - public static async Task StartBlazorApp(this AppBuilder builder, BrowserPlatformOptions? options = null) + public static async Task StartBlazorAppAsync(this AppBuilder builder, BrowserPlatformOptions? options = null) { options ??= new BrowserPlatformOptions(); options.FrameworkAssetPathResolver ??= filePath => $"/_content/Avalonia.Browser.Blazor/{filePath}"; diff --git a/src/Browser/Avalonia.Browser/BrowserAppBuilder.cs b/src/Browser/Avalonia.Browser/BrowserAppBuilder.cs index 866c8ceca4..32637b6d1b 100644 --- a/src/Browser/Avalonia.Browser/BrowserAppBuilder.cs +++ b/src/Browser/Avalonia.Browser/BrowserAppBuilder.cs @@ -21,7 +21,7 @@ public static class BrowserAppBuilder /// Application builder. /// ID of the html element where avalonia content should be rendered. /// Browser backend specific options. - public static async Task StartBrowserApp(this AppBuilder builder, string mainDivId, BrowserPlatformOptions? options = null) + public static async Task StartBrowserAppAsync(this AppBuilder builder, string mainDivId, BrowserPlatformOptions? options = null) { if (mainDivId is null) { @@ -46,9 +46,9 @@ public static class BrowserAppBuilder /// Browser backend specific options. /// /// This method doesn't creates any avalonia views to be rendered. To do so create an object. - /// Alternatively, you can call method instead of . + /// Alternatively, you can call method instead of . /// - public static async Task SetupBrowserApp(this AppBuilder builder, BrowserPlatformOptions? options = null) + public static async Task SetupBrowserAppAsync(this AppBuilder builder, BrowserPlatformOptions? options = null) { builder = await PreSetupBrowser(builder, options); From e6b8914b7d97b0c64f909e74832bf234c1c98032 Mon Sep 17 00:00:00 2001 From: Max Katz Date: Tue, 14 Feb 2023 20:13:46 -0500 Subject: [PATCH 32/49] Rename TryGetFileFromPath-like method to end with Async suffix --- .../ControlCatalog/Pages/DialogsPage.xaml.cs | 6 +-- .../Storage/FileIO/BclStorageProvider.cs | 6 +-- .../Platform/Storage/IStorageProvider.cs | 6 +-- .../Storage/StorageProviderExtensions.cs | 45 +++++++++---------- .../Platform/Dialogs/SystemDialogImpl.cs | 6 +-- .../Screenshots/FilePickerHandler.cs | 4 +- .../Internal/ManagedFileChooserViewModel.cs | 2 +- .../ManagedFileDialogExtensions.cs | 2 +- src/Avalonia.FreeDesktop/DBusSystemDialog.cs | 2 +- src/Avalonia.Native/SystemDialogs.cs | 6 +-- .../NativeDialogs/CompositeStorageProvider.cs | 12 ++--- .../NativeDialogs/GtkNativeFileDialogs.cs | 2 +- .../Avalonia.Win32/Win32StorageProvider.cs | 2 +- 13 files changed, 50 insertions(+), 51 deletions(-) diff --git a/samples/ControlCatalog/Pages/DialogsPage.xaml.cs b/samples/ControlCatalog/Pages/DialogsPage.xaml.cs index 61e10729cc..e24860e3e1 100644 --- a/samples/ControlCatalog/Pages/DialogsPage.xaml.cs +++ b/samples/ControlCatalog/Pages/DialogsPage.xaml.cs @@ -40,7 +40,7 @@ namespace ControlCatalog.Pages if (Enum.TryParse(currentFolderBox.Text, true, out var folderEnum)) { - lastSelectedDirectory = await GetStorageProvider().TryGetWellKnownFolder(folderEnum); + lastSelectedDirectory = await GetStorageProvider().TryGetWellKnownFolderAsync(folderEnum); } else { @@ -51,7 +51,7 @@ namespace ControlCatalog.Pages if (folderLink is not null) { - lastSelectedDirectory = await GetStorageProvider().TryGetFolderFromPath(folderLink); + lastSelectedDirectory = await GetStorageProvider().TryGetFolderFromPathAsync(folderLink); } } }; @@ -148,7 +148,7 @@ namespace ControlCatalog.Pages } else { - SetFolder(await GetStorageProvider().TryGetFolderFromPath(result)); + SetFolder(await GetStorageProvider().TryGetFolderFromPathAsync(result)); results.Items = new[] { result }; resultsVisible.IsVisible = true; } diff --git a/src/Avalonia.Base/Platform/Storage/FileIO/BclStorageProvider.cs b/src/Avalonia.Base/Platform/Storage/FileIO/BclStorageProvider.cs index ee169d62a5..34409f5fda 100644 --- a/src/Avalonia.Base/Platform/Storage/FileIO/BclStorageProvider.cs +++ b/src/Avalonia.Base/Platform/Storage/FileIO/BclStorageProvider.cs @@ -34,7 +34,7 @@ internal abstract class BclStorageProvider : IStorageProvider : Task.FromResult(null); } - public virtual Task TryGetFileFromPath(Uri filePath) + public virtual Task TryGetFileFromPathAsync(Uri filePath) { if (filePath.IsAbsoluteUri) { @@ -48,7 +48,7 @@ internal abstract class BclStorageProvider : IStorageProvider return Task.FromResult(null); } - public virtual Task TryGetFolderFromPath(Uri folderPath) + public virtual Task TryGetFolderFromPathAsync(Uri folderPath) { if (folderPath.IsAbsoluteUri) { @@ -62,7 +62,7 @@ internal abstract class BclStorageProvider : IStorageProvider return Task.FromResult(null); } - public virtual Task TryGetWellKnownFolder(WellKnownFolder wellKnownFolder) + public virtual Task TryGetWellKnownFolderAsync(WellKnownFolder wellKnownFolder) { // Note, this BCL API returns different values depending on the .NET version. // We should also document it. diff --git a/src/Avalonia.Base/Platform/Storage/IStorageProvider.cs b/src/Avalonia.Base/Platform/Storage/IStorageProvider.cs index 6922151e02..9d3c961e51 100644 --- a/src/Avalonia.Base/Platform/Storage/IStorageProvider.cs +++ b/src/Avalonia.Base/Platform/Storage/IStorageProvider.cs @@ -66,7 +66,7 @@ public interface IStorageProvider /// It also might ask user for the permission, and throw an exception if it was denied. /// /// File or null if it doesn't exist. - Task TryGetFileFromPath(Uri filePath); + Task TryGetFileFromPathAsync(Uri filePath); /// /// Attempts to read folder from the file-system by its path. @@ -78,12 +78,12 @@ public interface IStorageProvider /// It also might ask user for the permission, and throw an exception if it was denied. /// /// Folder or null if it doesn't exist. - Task TryGetFolderFromPath(Uri folderPath); + Task TryGetFolderFromPathAsync(Uri folderPath); /// /// Attempts to read folder from the file-system by its path /// /// Well known folder identifier. /// Folder or null if it doesn't exist. - Task TryGetWellKnownFolder(WellKnownFolder wellKnownFolder); + Task TryGetWellKnownFolderAsync(WellKnownFolder wellKnownFolder); } diff --git a/src/Avalonia.Base/Platform/Storage/StorageProviderExtensions.cs b/src/Avalonia.Base/Platform/Storage/StorageProviderExtensions.cs index c7772d1196..6f8b945cd6 100644 --- a/src/Avalonia.Base/Platform/Storage/StorageProviderExtensions.cs +++ b/src/Avalonia.Base/Platform/Storage/StorageProviderExtensions.cs @@ -8,48 +8,47 @@ namespace Avalonia.Platform.Storage; /// public static class StorageProviderExtensions { - /// - public static Task TryGetFileFromPath(this IStorageProvider provider, string filePath) + /// + public static Task TryGetFileFromPathAsync(this IStorageProvider provider, string filePath) { - return provider.TryGetFileFromPath(StorageProviderHelpers.FilePathToUri(filePath)); + return provider.TryGetFileFromPathAsync(StorageProviderHelpers.FilePathToUri(filePath)); } - /// - public static Task TryGetFolderFromPath(this IStorageProvider provider, string folderPath) + /// + public static Task TryGetFolderFromPathAsync(this IStorageProvider provider, string folderPath) { - return provider.TryGetFolderFromPath(StorageProviderHelpers.FilePathToUri(folderPath)); + return provider.TryGetFolderFromPathAsync(StorageProviderHelpers.FilePathToUri(folderPath)); } - internal static string? TryGetFullPath(this IStorageFolder folder) + /// + /// Gets the local file system path of the item as a string. + /// + /// Storage folder or file. + /// Full local path to the folder or file if possible, otherwise null. + /// + /// Android platform usually uses "content:" virtual file paths + /// and Browser platform has isolated access without full paths, + /// so on these platforms this method will return null. + /// + public static string? TryGetLocalPath(this IStorageItem item) { // We can avoid double escaping of the path by checking for BclStorageFolder. // Ideally, `folder.Path.LocalPath` should also work, as that's only available way for the users. - if (folder is BclStorageFolder storageFolder) + if (item is BclStorageFolder storageFolder) { return storageFolder.DirectoryInfo.FullName; } - - if (folder.Path is { IsAbsoluteUri: true, Scheme: "file" } absolutePath) - { - return absolutePath.LocalPath; - } - - // android "content:", browser and ios relative links go here. - return null; - } - - internal static string? TryGetFullPath(this IStorageFile file) - { - if (file is BclStorageFile storageFolder) + if (item is BclStorageFile storageFile) { - return storageFolder.FileInfo.FullName; + return storageFile.FileInfo.FullName; } - if (file.Path is { IsAbsoluteUri: true, Scheme: "file" } absolutePath) + if (item.Path is { IsAbsoluteUri: true, Scheme: "file" } absolutePath) { return absolutePath.LocalPath; } + // android "content:", browser and ios relative links go here. return null; } } diff --git a/src/Avalonia.Controls/Platform/Dialogs/SystemDialogImpl.cs b/src/Avalonia.Controls/Platform/Dialogs/SystemDialogImpl.cs index a8a266e378..20bfb440e3 100644 --- a/src/Avalonia.Controls/Platform/Dialogs/SystemDialogImpl.cs +++ b/src/Avalonia.Controls/Platform/Dialogs/SystemDialogImpl.cs @@ -27,7 +27,7 @@ namespace Avalonia.Controls.Platform var files = await filePicker.OpenFilePickerAsync(options); return files - .Select(file => file.TryGetFullPath() ?? file.Name) + .Select(file => file.TryGetLocalPath() ?? file.Name) .ToArray(); } else if (dialog is SaveFileDialog saveDialog) @@ -46,7 +46,7 @@ namespace Avalonia.Controls.Platform return null; } - var filePath = file.TryGetFullPath() ?? file.Name; + var filePath = file.TryGetLocalPath() ?? file.Name; return new[] { filePath }; } return null; @@ -64,7 +64,7 @@ namespace Avalonia.Controls.Platform var folders = await filePicker.OpenFolderPickerAsync(options); return folders - .Select(folder => folder.TryGetFullPath() ?? folder.Name) + .Select(folder => folder.TryGetLocalPath() ?? folder.Name) .FirstOrDefault(u => u is not null); } } diff --git a/src/Avalonia.Diagnostics/Diagnostics/Screenshots/FilePickerHandler.cs b/src/Avalonia.Diagnostics/Diagnostics/Screenshots/FilePickerHandler.cs index 548d177643..6ea46b6d54 100644 --- a/src/Avalonia.Diagnostics/Diagnostics/Screenshots/FilePickerHandler.cs +++ b/src/Avalonia.Diagnostics/Diagnostics/Screenshots/FilePickerHandler.cs @@ -54,8 +54,8 @@ namespace Avalonia.Diagnostics.Screenshots protected override async Task GetStream(Control control) { var storageProvider = GetTopLevel(control).StorageProvider; - var defaultFolder = await storageProvider.TryGetFolderFromPath(_screenshotRoot) - ?? await storageProvider.TryGetWellKnownFolder(WellKnownFolder.Pictures); + var defaultFolder = await storageProvider.TryGetFolderFromPathAsync(_screenshotRoot) + ?? await storageProvider.TryGetWellKnownFolderAsync(WellKnownFolder.Pictures); var result = await storageProvider.SaveFilePickerAsync(new FilePickerSaveOptions { diff --git a/src/Avalonia.Dialogs/Internal/ManagedFileChooserViewModel.cs b/src/Avalonia.Dialogs/Internal/ManagedFileChooserViewModel.cs index 46de460b1a..c929cb9ac9 100644 --- a/src/Avalonia.Dialogs/Internal/ManagedFileChooserViewModel.cs +++ b/src/Avalonia.Dialogs/Internal/ManagedFileChooserViewModel.cs @@ -260,7 +260,7 @@ namespace Avalonia.Dialogs.Internal public void Navigate(IStorageFolder path, string initialSelectionName = null) { - var fullDirectoryPath = path?.TryGetFullPath() ?? Directory.GetCurrentDirectory(); + var fullDirectoryPath = path?.TryGetLocalPath() ?? Directory.GetCurrentDirectory(); Navigate(fullDirectoryPath, initialSelectionName); } diff --git a/src/Avalonia.Dialogs/ManagedFileDialogExtensions.cs b/src/Avalonia.Dialogs/ManagedFileDialogExtensions.cs index cad938ac35..e9a75ab46a 100644 --- a/src/Avalonia.Dialogs/ManagedFileDialogExtensions.cs +++ b/src/Avalonia.Dialogs/ManagedFileDialogExtensions.cs @@ -51,7 +51,7 @@ namespace Avalonia.Dialogs var files = await impl.OpenFilePickerAsync(dialog.ToFilePickerOpenOptions()); return files - .Select(file => file.TryGetFullPath() ?? file.Name) + .Select(file => file.TryGetLocalPath() ?? file.Name) .ToArray(); } } diff --git a/src/Avalonia.FreeDesktop/DBusSystemDialog.cs b/src/Avalonia.FreeDesktop/DBusSystemDialog.cs index 6f647215be..905ce1f272 100644 --- a/src/Avalonia.FreeDesktop/DBusSystemDialog.cs +++ b/src/Avalonia.FreeDesktop/DBusSystemDialog.cs @@ -88,7 +88,7 @@ namespace Avalonia.FreeDesktop if (options.SuggestedFileName is { } currentName) chooserOptions.Add("current_name", currentName); - if (options.SuggestedStartLocation?.TryGetFullPath() is { } folderPath) + if (options.SuggestedStartLocation?.TryGetLocalPath() is { } folderPath) chooserOptions.Add("current_folder", Encoding.UTF8.GetBytes(folderPath)); objectPath = await _fileChooser.SaveFileAsync(parentWindow, options.Title ?? string.Empty, chooserOptions); diff --git a/src/Avalonia.Native/SystemDialogs.cs b/src/Avalonia.Native/SystemDialogs.cs index 60724507f3..1d23ad9997 100644 --- a/src/Avalonia.Native/SystemDialogs.cs +++ b/src/Avalonia.Native/SystemDialogs.cs @@ -33,7 +33,7 @@ namespace Avalonia.Native { using var events = new SystemDialogEvents(); - var suggestedDirectory = options.SuggestedStartLocation?.TryGetFullPath() ?? string.Empty; + var suggestedDirectory = options.SuggestedStartLocation?.TryGetLocalPath() ?? string.Empty; _native.OpenFileDialog((IAvnWindow)_window.Native, events, @@ -53,7 +53,7 @@ namespace Avalonia.Native { using var events = new SystemDialogEvents(); - var suggestedDirectory = options.SuggestedStartLocation?.TryGetFullPath() ?? string.Empty; + var suggestedDirectory = options.SuggestedStartLocation?.TryGetLocalPath() ?? string.Empty; _native.SaveFileDialog((IAvnWindow)_window.Native, events, @@ -72,7 +72,7 @@ namespace Avalonia.Native { using var events = new SystemDialogEvents(); - var suggestedDirectory = options.SuggestedStartLocation?.TryGetFullPath() ?? string.Empty; + var suggestedDirectory = options.SuggestedStartLocation?.TryGetLocalPath() ?? string.Empty; _native.SelectFolderDialog((IAvnWindow)_window.Native, events, options.AllowMultiple.AsComBool(), options.Title ?? "", suggestedDirectory); diff --git a/src/Avalonia.X11/NativeDialogs/CompositeStorageProvider.cs b/src/Avalonia.X11/NativeDialogs/CompositeStorageProvider.cs index 07a11ff2ec..16416c7e56 100644 --- a/src/Avalonia.X11/NativeDialogs/CompositeStorageProvider.cs +++ b/src/Avalonia.X11/NativeDialogs/CompositeStorageProvider.cs @@ -62,21 +62,21 @@ internal class CompositeStorageProvider : IStorageProvider return await provider.OpenFolderBookmarkAsync(bookmark).ConfigureAwait(false); } - public async Task TryGetFileFromPath(Uri filePath) + public async Task TryGetFileFromPathAsync(Uri filePath) { var provider = await EnsureStorageProvider().ConfigureAwait(false); - return await provider.TryGetFileFromPath(filePath).ConfigureAwait(false); + return await provider.TryGetFileFromPathAsync(filePath).ConfigureAwait(false); } - public async Task TryGetFolderFromPath(Uri folderPath) + public async Task TryGetFolderFromPathAsync(Uri folderPath) { var provider = await EnsureStorageProvider().ConfigureAwait(false); - return await provider.TryGetFolderFromPath(folderPath).ConfigureAwait(false); + return await provider.TryGetFolderFromPathAsync(folderPath).ConfigureAwait(false); } - public async Task TryGetWellKnownFolder(WellKnownFolder wellKnownFolder) + public async Task TryGetWellKnownFolderAsync(WellKnownFolder wellKnownFolder) { var provider = await EnsureStorageProvider().ConfigureAwait(false); - return await provider.TryGetWellKnownFolder(wellKnownFolder).ConfigureAwait(false); + return await provider.TryGetWellKnownFolderAsync(wellKnownFolder).ConfigureAwait(false); } } diff --git a/src/Avalonia.X11/NativeDialogs/GtkNativeFileDialogs.cs b/src/Avalonia.X11/NativeDialogs/GtkNativeFileDialogs.cs index ceb32e52e9..16e1a1bce8 100644 --- a/src/Avalonia.X11/NativeDialogs/GtkNativeFileDialogs.cs +++ b/src/Avalonia.X11/NativeDialogs/GtkNativeFileDialogs.cs @@ -196,7 +196,7 @@ namespace Avalonia.X11.NativeDialogs gtk_dialog_add_button(dlg, open, GtkResponseType.Cancel); } - var folderLocalPath = initialFolder?.TryGetFullPath(); + var folderLocalPath = initialFolder?.TryGetLocalPath(); if (folderLocalPath is not null) { using var dir = new Utf8Buffer(folderLocalPath); diff --git a/src/Windows/Avalonia.Win32/Win32StorageProvider.cs b/src/Windows/Avalonia.Win32/Win32StorageProvider.cs index 2fd49c8b09..86574f2fa3 100644 --- a/src/Windows/Avalonia.Win32/Win32StorageProvider.cs +++ b/src/Windows/Avalonia.Win32/Win32StorageProvider.cs @@ -131,7 +131,7 @@ namespace Avalonia.Win32 } } - if (folder?.TryGetFullPath() is { } folderPath) + if (folder?.TryGetLocalPath() is { } folderPath) { var riid = UnmanagedMethods.ShellIds.IShellItem; if (UnmanagedMethods.SHCreateItemFromParsingName(folderPath, IntPtr.Zero, ref riid, out var directoryShellItem) From dd1709732324739db36ce30fbc7e8d45c091b172 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Wed, 15 Feb 2023 10:05:17 +0100 Subject: [PATCH 33/49] Added failing test for #10255 . --- .../AvaloniaObjectTests_Binding.cs | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_Binding.cs b/tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_Binding.cs index baaed5104d..b6036bba8f 100644 --- a/tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_Binding.cs +++ b/tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_Binding.cs @@ -167,6 +167,22 @@ namespace Avalonia.Base.UnitTests Assert.Equal("foo", target.GetValue(property)); } + [Fact] + public void Completing_Animation_Binding_Reverts_To_Set_LocalValue_With_Style_Value() + { + var target = new Class1(); + var source = new Subject>(); + var property = Class1.FooProperty; + + target.SetValue(property, "style", BindingPriority.Style); + target.SetValue(property, "foo"); + target.Bind(property, source, BindingPriority.Animation); + source.OnNext("bar"); + source.OnCompleted(); + + Assert.Equal("foo", target.GetValue(property)); + } + [Fact] public void Completing_LocalValue_Binding_Raises_PropertyChanged() { From 3e81ed8fbca08a36eeec53db5792a31440283d2f Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Wed, 15 Feb 2023 11:04:03 +0100 Subject: [PATCH 34/49] Revert to LocalValue when animation completes. Fixes #10255 . --- src/Avalonia.Base/PropertyStore/ValueStore.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Avalonia.Base/PropertyStore/ValueStore.cs b/src/Avalonia.Base/PropertyStore/ValueStore.cs index 8b702665f8..7734369ff0 100644 --- a/src/Avalonia.Base/PropertyStore/ValueStore.cs +++ b/src/Avalonia.Base/PropertyStore/ValueStore.cs @@ -799,7 +799,7 @@ namespace Avalonia.PropertyStore // - The value is a non-animation value and its priority is higher than the current // effective value's base priority var isRelevantPriority = current is null || - priority < current.Priority || + (priority < current.Priority && priority < current.BasePriority) || (priority > BindingPriority.Animation && priority < current.BasePriority); if (foundEntry && isRelevantPriority && entry!.HasValue) From 2cff53c05713f6d52d4a3120a0cec77ea238911d Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Wed, 15 Feb 2023 11:05:34 +0100 Subject: [PATCH 35/49] Added failing test for #10345. --- .../ValueStoreTests_Inheritance.cs | 21 +++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/tests/Avalonia.Base.UnitTests/PropertyStore/ValueStoreTests_Inheritance.cs b/tests/Avalonia.Base.UnitTests/PropertyStore/ValueStoreTests_Inheritance.cs index ed122e4ddc..91b0f9cdee 100644 --- a/tests/Avalonia.Base.UnitTests/PropertyStore/ValueStoreTests_Inheritance.cs +++ b/tests/Avalonia.Base.UnitTests/PropertyStore/ValueStoreTests_Inheritance.cs @@ -97,6 +97,27 @@ namespace Avalonia.Base.UnitTests.PropertyStore Assert.Same(parentStore, grandchild.GetValueStore().InheritanceAncestor); } + [Fact] + public void Child_Notifies_About_Setting_Back_To_Default_Value() + { + var parent = new Class1(); + var child = new Class1(); + + parent.Foo = "changed"; + child.Parent = parent; + + bool raised = false; + child.PropertyChanged += (_, args) => + { + raised = args.Property == Class1.FooProperty && args.GetNewValue() == "foodefault"; + }; + + Assert.Equal("changed", child.Foo); // inherited from parent. + + child.Foo = "foodefault"; // reset back to default. + Assert.True(raised); // expect event to be raised, as actual value was changed. + } + [Fact] public void Adding_Child_Sets_InheritanceAncestor() { From 994897a0238caf1882060f41e0e4634efda4a34a Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Wed, 15 Feb 2023 11:32:56 +0100 Subject: [PATCH 36/49] Initialize EffectiveValue with inherited value. Fixes #10345 --- .../PropertyStore/EffectiveValue`1.cs | 7 +++++-- src/Avalonia.Base/PropertyStore/ValueStore.cs | 16 +++++++++++++--- src/Avalonia.Base/StyledProperty.cs | 2 +- 3 files changed, 19 insertions(+), 6 deletions(-) diff --git a/src/Avalonia.Base/PropertyStore/EffectiveValue`1.cs b/src/Avalonia.Base/PropertyStore/EffectiveValue`1.cs index 0d93e9d8ed..c469034f9b 100644 --- a/src/Avalonia.Base/PropertyStore/EffectiveValue`1.cs +++ b/src/Avalonia.Base/PropertyStore/EffectiveValue`1.cs @@ -19,13 +19,16 @@ namespace Avalonia.PropertyStore private T? _baseValue; private UncommonFields? _uncommon; - public EffectiveValue(AvaloniaObject owner, StyledProperty property) + public EffectiveValue( + AvaloniaObject owner, + StyledProperty property, + EffectiveValue? inherited) { Priority = BindingPriority.Unset; BasePriority = BindingPriority.Unset; _metadata = property.GetMetadata(owner.GetType()); - var value = _metadata.DefaultValue; + var value = inherited is null ? _metadata.DefaultValue : inherited.Value; if (property.HasCoercion && _metadata.CoerceValue is { } coerce) { diff --git a/src/Avalonia.Base/PropertyStore/ValueStore.cs b/src/Avalonia.Base/PropertyStore/ValueStore.cs index 7734369ff0..ec6ed392c1 100644 --- a/src/Avalonia.Base/PropertyStore/ValueStore.cs +++ b/src/Avalonia.Base/PropertyStore/ValueStore.cs @@ -184,7 +184,7 @@ namespace Avalonia.PropertyStore } else { - var effectiveValue = new EffectiveValue(Owner, property); + var effectiveValue = CreateEffectiveValue(property); AddEffectiveValue(property, effectiveValue); effectiveValue.SetAndRaise(this, result, priority); } @@ -200,7 +200,7 @@ namespace Avalonia.PropertyStore } else { - var effectiveValue = new EffectiveValue(Owner, property); + var effectiveValue = CreateEffectiveValue(property); AddEffectiveValue(property, effectiveValue); effectiveValue.SetLocalValueAndRaise(this, property, value); } @@ -217,7 +217,7 @@ namespace Avalonia.PropertyStore } else { - var effectiveValue = new EffectiveValue(Owner, property); + var effectiveValue = CreateEffectiveValue(property); AddEffectiveValue(property, effectiveValue); effectiveValue.SetCurrentValueAndRaise(this, property, value); } @@ -287,6 +287,16 @@ namespace Avalonia.PropertyStore return false; } + public EffectiveValue CreateEffectiveValue(StyledProperty property) + { + EffectiveValue? inherited = null; + + if (property.Inherits && TryGetInheritedValue(property, out var v)) + inherited = (EffectiveValue)v; + + return new EffectiveValue(Owner, property, inherited); + } + public void SetInheritanceParent(AvaloniaObject? newParent) { var values = AvaloniaPropertyDictionaryPool.Get(); diff --git a/src/Avalonia.Base/StyledProperty.cs b/src/Avalonia.Base/StyledProperty.cs index ad1f09066e..8695918c18 100644 --- a/src/Avalonia.Base/StyledProperty.cs +++ b/src/Avalonia.Base/StyledProperty.cs @@ -171,7 +171,7 @@ namespace Avalonia internal override EffectiveValue CreateEffectiveValue(AvaloniaObject o) { - return new EffectiveValue(o, this); + return o.GetValueStore().CreateEffectiveValue(this); } /// From bfafd06cdeb8b2e22e46f51d05c5b9339212e503 Mon Sep 17 00:00:00 2001 From: Benedikt Stebner Date: Wed, 15 Feb 2023 13:34:20 +0100 Subject: [PATCH 37/49] Do not register headless font manager and shaper --- src/Avalonia.Headless/AvaloniaHeadlessPlatform.cs | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/Avalonia.Headless/AvaloniaHeadlessPlatform.cs b/src/Avalonia.Headless/AvaloniaHeadlessPlatform.cs index 9834969898..b0b1d731d2 100644 --- a/src/Avalonia.Headless/AvaloniaHeadlessPlatform.cs +++ b/src/Avalonia.Headless/AvaloniaHeadlessPlatform.cs @@ -68,8 +68,6 @@ namespace Avalonia.Headless .Bind().ToConstant(new KeyboardDevice()) .Bind().ToConstant(new RenderLoop()) .Bind().ToConstant(new RenderTimer(60)) - .Bind().ToSingleton() - .Bind().ToSingleton() .Bind().ToConstant(new HeadlessWindowingPlatform()) .Bind().ToSingleton(); Compositor = new Compositor(AvaloniaLocator.Current.GetRequiredService(), null); From 44b833a0af0645b3d99757e3d900834dfdeffd13 Mon Sep 17 00:00:00 2001 From: Daniil Pavliuchyk Date: Wed, 15 Feb 2023 15:20:05 +0200 Subject: [PATCH 38/49] Fix test --- samples/IntegrationTestApp/MainWindow.axaml | 4 ++-- tests/Avalonia.IntegrationTests.Appium/SliderTests.cs | 8 ++++---- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/samples/IntegrationTestApp/MainWindow.axaml b/samples/IntegrationTestApp/MainWindow.axaml index 8fd33061df..72470873cb 100644 --- a/samples/IntegrationTestApp/MainWindow.axaml +++ b/samples/IntegrationTestApp/MainWindow.axaml @@ -151,8 +151,8 @@ - - + + diff --git a/tests/Avalonia.IntegrationTests.Appium/SliderTests.cs b/tests/Avalonia.IntegrationTests.Appium/SliderTests.cs index f6c50af59d..7fa5eb83ee 100644 --- a/tests/Avalonia.IntegrationTests.Appium/SliderTests.cs +++ b/tests/Avalonia.IntegrationTests.Appium/SliderTests.cs @@ -15,19 +15,19 @@ namespace Avalonia.IntegrationTests.Appium _session = fixture.Session; var tabs = _session.FindElementByAccessibilityId("MainTabs"); - var tab = tabs.FindElementByName("Slider"); + var tab = tabs.FindElementByName("SliderTab"); tab.Click(); } [Fact] - public void Changes_Value_When_Moving_Slider() + public void Changes_Value_When_Clicking_Increase_Button() { - var slider = _session.FindElementByAccessibilityId("Slider2"); + var slider = _session.FindElementByAccessibilityId("Slider"); // slider.Text gets the Slider value Assert.True(double.Parse(slider.Text) == 30); - new Actions(_session).Click(slider).MoveByOffset(100, 0).Perform(); + new Actions(_session).Click(slider).Perform(); Assert.Equal(50, Math.Round(double.Parse(slider.Text))); } From c2cd05a5f07b391465633bb2b4683a7758d5eb22 Mon Sep 17 00:00:00 2001 From: Julien Lebosquain Date: Wed, 15 Feb 2023 18:03:52 +0100 Subject: [PATCH 39/49] Nullable annotations for Avalonia.OpenGL --- src/Avalonia.OpenGL/Avalonia.OpenGL.csproj | 1 + .../Controls/CompositionOpenGlSwapchain.cs | 5 +-- .../Controls/OpenGlControlBase.cs | 33 +++++++------- .../Controls/OpenGlControlResources.cs | 6 +-- src/Avalonia.OpenGL/Egl/EglContext.cs | 22 +++++----- src/Avalonia.OpenGL/Egl/EglDisplay.cs | 11 +++-- src/Avalonia.OpenGL/Egl/EglDisplayOptions.cs | 18 ++++---- src/Avalonia.OpenGL/Egl/EglDisplayUtils.cs | 11 +++-- .../Egl/EglGlPlatformSurface.cs | 8 ++-- .../Egl/EglGlPlatformSurfaceBase.cs | 11 +++-- src/Avalonia.OpenGL/Egl/EglInterface.cs | 10 ++--- .../Egl/EglPlatformGraphics.cs | 5 +-- .../ExternalObjectsOpenGlExtensionFeature.cs | 43 +++++++++++-------- src/Avalonia.OpenGL/GlBasicInfoInterface.cs | 16 ++++--- src/Avalonia.OpenGL/GlInterface.cs | 18 ++++---- src/Avalonia.OpenGL/IGlContext.cs | 2 +- ...ureSharingRenderInterfaceContextFeature.cs | 3 +- src/Avalonia.OpenGL/OpenGlException.cs | 6 +-- 18 files changed, 121 insertions(+), 108 deletions(-) diff --git a/src/Avalonia.OpenGL/Avalonia.OpenGL.csproj b/src/Avalonia.OpenGL/Avalonia.OpenGL.csproj index 73493c69c0..4dd7432082 100644 --- a/src/Avalonia.OpenGL/Avalonia.OpenGL.csproj +++ b/src/Avalonia.OpenGL/Avalonia.OpenGL.csproj @@ -13,6 +13,7 @@ + diff --git a/src/Avalonia.OpenGL/Controls/CompositionOpenGlSwapchain.cs b/src/Avalonia.OpenGL/Controls/CompositionOpenGlSwapchain.cs index 3db0aafd7b..42b1af7b25 100644 --- a/src/Avalonia.OpenGL/Controls/CompositionOpenGlSwapchain.cs +++ b/src/Avalonia.OpenGL/Controls/CompositionOpenGlSwapchain.cs @@ -1,5 +1,4 @@ using System; -using System.Collections.Generic; using System.Threading.Tasks; using Avalonia.Platform; using Avalonia.Rendering; @@ -22,7 +21,7 @@ internal class CompositionOpenGlSwapchain : SwapchainBase } public CompositionOpenGlSwapchain(IGlContext context, ICompositionGpuInterop interop, CompositionDrawingSurface target, - IGlContextExternalObjectsFeature externalObjectsFeature) : base(interop, target) + IGlContextExternalObjectsFeature? externalObjectsFeature) : base(interop, target) { _context = context; _externalObjectsFeature = externalObjectsFeature; @@ -95,7 +94,7 @@ internal class DxgiMutexOpenGlSwapChainImage : IGlSwapchainImage public int TextureId => _texture.TextureId; public int InternalFormat => _texture.InternalFormat; public PixelSize Size => new(_texture.Properties.Width, _texture.Properties.Height); - public Task LastPresent => _lastPresent; + public Task? LastPresent => _lastPresent; public void BeginDraw() => _texture.AcquireKeyedMutex(0); public void Present() diff --git a/src/Avalonia.OpenGL/Controls/OpenGlControlBase.cs b/src/Avalonia.OpenGL/Controls/OpenGlControlBase.cs index 4d0663a12b..7e73397743 100644 --- a/src/Avalonia.OpenGL/Controls/OpenGlControlBase.cs +++ b/src/Avalonia.OpenGL/Controls/OpenGlControlBase.cs @@ -1,4 +1,5 @@ using System; +using System.Diagnostics.CodeAnalysis; using System.Numerics; using System.Threading.Tasks; using Avalonia.Controls; @@ -11,10 +12,10 @@ namespace Avalonia.OpenGL.Controls { public abstract class OpenGlControlBase : Control { - private CompositionSurfaceVisual _visual; - private Action _update; + private CompositionSurfaceVisual? _visual; + private readonly Action _update; private bool _updateQueued; - private Task _initialization; + private Task? _initialization; private OpenGlControlBaseResources? _resources; private Compositor? _compositor; protected GlVersion GlVersion => _resources?.Context.Version ?? default; @@ -24,7 +25,7 @@ namespace Avalonia.OpenGL.Controls _update = Update; } - void DoCleanup() + private void DoCleanup() { if (_initialization is { Status: TaskStatus.RanToCompletion } && _resources != null) { @@ -63,14 +64,14 @@ namespace Avalonia.OpenGL.Controls RequestNextFrameRendering(); } + [MemberNotNullWhen(true, nameof(_resources))] private bool EnsureInitializedCore( ICompositionGpuInterop interop, - IOpenGlTextureSharingRenderInterfaceContextFeature contextSharingFeature) + IOpenGlTextureSharingRenderInterfaceContextFeature? contextSharingFeature) { - var surface = _compositor.CreateDrawingSurface(); + var surface = _compositor!.CreateDrawingSurface(); - IGlContext ctx = null; - var contextFactory = AvaloniaLocator.Current.GetService(); + IGlContext? ctx = null; try { if (contextSharingFeature?.CanCreateSharedContext == true) @@ -78,6 +79,7 @@ namespace Avalonia.OpenGL.Controls if(_resources == null) { + var contextFactory = AvaloniaLocator.Current.GetRequiredService(); ctx = contextFactory.CreateContext(null); if (ctx.TryGetFeature(out var externalObjects)) _resources = OpenGlControlBaseResources.TryCreate(ctx, surface, interop, externalObjects); @@ -121,13 +123,14 @@ namespace Avalonia.OpenGL.Controls base.OnPropertyChanged(change); } - void ContextLost() + private void ContextLost() { _initialization = null; _resources?.DisposeAsync(); OnOpenGlLost(); } + [MemberNotNullWhen(true, nameof(_resources))] private bool EnsureInitialized() { if (_initialization != null) @@ -170,11 +173,11 @@ namespace Avalonia.OpenGL.Controls private void Update() { _updateQueued = false; - if (VisualRoot == null) + if (VisualRoot is not { } visualRoot) return; if(!EnsureInitialized()) return; - using (_resources.BeginDraw(GetPixelSize())) + using (_resources.BeginDraw(GetPixelSize(visualRoot))) OnOpenGlRender(_resources.Context.GlInterface, _resources.Fbo); } @@ -190,7 +193,7 @@ namespace Avalonia.OpenGL.Controls var gpuInteropTask = _compositor.TryGetCompositionGpuInterop(); var contextSharingFeature = - (IOpenGlTextureSharingRenderInterfaceContextFeature) + (IOpenGlTextureSharingRenderInterfaceContextFeature?) await _compositor.TryGetRenderInterfaceFeature( typeof(IOpenGlTextureSharingRenderInterfaceContextFeature)); var interop = await gpuInteropTask; @@ -208,7 +211,7 @@ namespace Avalonia.OpenGL.Controls return false; } - using (_resources!.Context.MakeCurrent()) + using (_resources.Context.MakeCurrent()) OnOpenGlInit(_resources.Context.GlInterface); return true; @@ -228,9 +231,9 @@ namespace Avalonia.OpenGL.Controls } } - private PixelSize GetPixelSize() + private PixelSize GetPixelSize(IRenderRoot visualRoot) { - var scaling = VisualRoot!.RenderScaling; + var scaling = visualRoot.RenderScaling; return new PixelSize(Math.Max(1, (int)(Bounds.Width * scaling)), Math.Max(1, (int)(Bounds.Height * scaling))); } diff --git a/src/Avalonia.OpenGL/Controls/OpenGlControlResources.cs b/src/Avalonia.OpenGL/Controls/OpenGlControlResources.cs index bda2bd1568..3b0b3bd028 100644 --- a/src/Avalonia.OpenGL/Controls/OpenGlControlResources.cs +++ b/src/Avalonia.OpenGL/Controls/OpenGlControlResources.cs @@ -14,14 +14,14 @@ internal class OpenGlControlBaseResources : IAsyncDisposable public int Fbo { get; private set; } private PixelSize _depthBufferSize; public CompositionDrawingSurface Surface { get; } - public CompositionOpenGlSwapchain _swapchain; + private readonly CompositionOpenGlSwapchain _swapchain; public IGlContext Context { get; private set; } public static OpenGlControlBaseResources? TryCreate(CompositionDrawingSurface surface, ICompositionGpuInterop interop, IOpenGlTextureSharingRenderInterfaceContextFeature feature) { - IGlContext context; + IGlContext? context; try { context = feature.CreateSharedContext(); @@ -71,7 +71,7 @@ internal class OpenGlControlBaseResources : IAsyncDisposable new CompositionOpenGlSwapchain(context, interop, Surface, externalObjects); } - void UpdateDepthRenderbuffer(PixelSize size) + private void UpdateDepthRenderbuffer(PixelSize size) { if (size == _depthBufferSize && _depthBuffer != 0) return; diff --git a/src/Avalonia.OpenGL/Egl/EglContext.cs b/src/Avalonia.OpenGL/Egl/EglContext.cs index 48dfc6c4c0..936ec06c1d 100644 --- a/src/Avalonia.OpenGL/Egl/EglContext.cs +++ b/src/Avalonia.OpenGL/Egl/EglContext.cs @@ -12,15 +12,15 @@ namespace Avalonia.OpenGL.Egl { private readonly EglDisplay _disp; private readonly EglInterface _egl; - private readonly EglContext _sharedWith; + private readonly EglContext? _sharedWith; private bool _isLost; private IntPtr _context; - private readonly Action _disposeCallback; + private readonly Action? _disposeCallback; private readonly Dictionary _features; private readonly object _lock; - internal EglContext(EglDisplay display, EglInterface egl, EglContext sharedWith, IntPtr ctx, EglSurface offscreenSurface, - GlVersion version, int sampleCount, int stencilSize, Action disposeCallback, + internal EglContext(EglDisplay display, EglInterface egl, EglContext? sharedWith, IntPtr ctx, EglSurface? offscreenSurface, + GlVersion version, int sampleCount, int stencilSize, Action? disposeCallback, Dictionary> features) { _disp = display; @@ -42,19 +42,19 @@ namespace Avalonia.OpenGL.Egl public IntPtr Context => _context == IntPtr.Zero ? throw new ObjectDisposedException(nameof(EglContext)) : _context; - public EglSurface OffscreenSurface { get; } + public EglSurface? OffscreenSurface { get; } public GlVersion Version { get; } public GlInterface GlInterface { get; } public int SampleCount { get; } public int StencilSize { get; } public EglDisplay Display => _disp; - class RestoreContext : IDisposable + private class RestoreContext : IDisposable { private readonly EglInterface _egl; private readonly object _l; private readonly IntPtr _display; - public IntPtr _context, _read, _draw; + private readonly IntPtr _context, _read, _draw; public RestoreContext(EglInterface egl, IntPtr defDisplay, object l) { @@ -78,7 +78,7 @@ namespace Avalonia.OpenGL.Egl public IDisposable MakeCurrent() => MakeCurrent(OffscreenSurface); - public IDisposable MakeCurrent(EglSurface surface) + public IDisposable MakeCurrent(EglSurface? surface) { if (IsLost) throw new PlatformGraphicsContextLostException(); @@ -147,7 +147,7 @@ namespace Avalonia.OpenGL.Egl public bool CanCreateSharedContext => _disp.SupportsSharing; - public IGlContext CreateSharedContext(IEnumerable preferredVersions = null) => + public IGlContext CreateSharedContext(IEnumerable? preferredVersions = null) => _disp.CreateContext(new EglContextOptions { ShareWith = _sharedWith ?? this @@ -174,9 +174,9 @@ namespace Avalonia.OpenGL.Egl _disposeCallback?.Invoke(); } - public object TryGetFeature(Type featureType) + public object? TryGetFeature(Type featureType) { - if (_features?.TryGetValue(featureType, out var feature) == true) + if (_features.TryGetValue(featureType, out var feature)) return feature; return null; } diff --git a/src/Avalonia.OpenGL/Egl/EglDisplay.cs b/src/Avalonia.OpenGL/Egl/EglDisplay.cs index 60cad2fa1e..db1fe56139 100644 --- a/src/Avalonia.OpenGL/Egl/EglDisplay.cs +++ b/src/Avalonia.OpenGL/Egl/EglDisplay.cs @@ -14,14 +14,14 @@ namespace Avalonia.OpenGL.Egl private readonly EglDisplayOptions _options; private EglConfigInfo _config; private bool _isLost; - private object _lock = new(); + private readonly object _lock = new(); public bool SupportsSharing { get; } public IntPtr Handle => _display; public IntPtr Config => _config.Config; internal bool SingleContext => !_options.SupportsMultipleContexts; - private List _contexts = new(); + private readonly List _contexts = new(); public EglDisplay() : this(new EglDisplayCreationOptions { @@ -38,7 +38,7 @@ namespace Avalonia.OpenGL.Egl public EglDisplay(IntPtr display, EglDisplayOptions options) { - _egl = options.Egl; + _egl = options.Egl ?? new EglInterface(); SupportsSharing = options.SupportsContextSharing; _display = display; _options = options; @@ -49,7 +49,7 @@ namespace Avalonia.OpenGL.Egl } public EglInterface EglInterface => _egl; - public EglContext CreateContext(EglContextOptions options) + public EglContext CreateContext(EglContextOptions? options) { if (SingleContext && _contexts.Any()) throw new OpenGlException("This EGLDisplay can only have one active context"); @@ -129,7 +129,7 @@ namespace Avalonia.OpenGL.Egl protected virtual bool DisplayLockIsSharedWithContexts => false; - internal object ContextSharedSyncRoot => DisplayLockIsSharedWithContexts ? _lock : null; + internal object? ContextSharedSyncRoot => DisplayLockIsSharedWithContexts ? _lock : null; internal void OnContextLost(EglContext context) { @@ -171,7 +171,6 @@ namespace Avalonia.OpenGL.Egl if (_display != IntPtr.Zero) _egl.Terminate(_display); _display = IntPtr.Zero; - _config = null; _options.DisposeCallback?.Invoke(); } } diff --git a/src/Avalonia.OpenGL/Egl/EglDisplayOptions.cs b/src/Avalonia.OpenGL/Egl/EglDisplayOptions.cs index 906a533e2d..a2cbeb5b3d 100644 --- a/src/Avalonia.OpenGL/Egl/EglDisplayOptions.cs +++ b/src/Avalonia.OpenGL/Egl/EglDisplayOptions.cs @@ -5,26 +5,26 @@ namespace Avalonia.OpenGL.Egl; public class EglDisplayOptions { - public EglInterface Egl { get; set; } + public EglInterface? Egl { get; set; } public bool SupportsContextSharing { get; set; } public bool SupportsMultipleContexts { get; set; } public bool ContextLossIsDisplayLoss { get; set; } - public Func DeviceLostCheckCallback { get; set; } - public Action DisposeCallback { get; set; } - public IEnumerable GlVersions { get; set; } + public Func? DeviceLostCheckCallback { get; set; } + public Action? DisposeCallback { get; set; } + public IEnumerable? GlVersions { get; set; } } public class EglContextOptions { - public EglContext ShareWith { get; set; } - public EglSurface OffscreenSurface { get; set; } - public Action DisposeCallback { get; set; } - public Dictionary> ExtraFeatures { get; set; } + public EglContext? ShareWith { get; set; } + public EglSurface? OffscreenSurface { get; set; } + public Action? DisposeCallback { get; set; } + public Dictionary>? ExtraFeatures { get; set; } } public class EglDisplayCreationOptions : EglDisplayOptions { public int? PlatformType { get; set; } public IntPtr PlatformDisplay { get; set; } - public int[] PlatformDisplayAttrs { get; set; } + public int[]? PlatformDisplayAttrs { get; set; } } diff --git a/src/Avalonia.OpenGL/Egl/EglDisplayUtils.cs b/src/Avalonia.OpenGL/Egl/EglDisplayUtils.cs index 5573eb39fa..0adf114b54 100644 --- a/src/Avalonia.OpenGL/Egl/EglDisplayUtils.cs +++ b/src/Avalonia.OpenGL/Egl/EglDisplayUtils.cs @@ -1,15 +1,14 @@ using System; using System.Collections.Generic; using System.Linq; -using System.Threading; using static Avalonia.OpenGL.Egl.EglConsts; namespace Avalonia.OpenGL.Egl; -static class EglDisplayUtils +internal static class EglDisplayUtils { public static IntPtr CreateDisplay(EglDisplayCreationOptions options) { - var egl = options.Egl; + var egl = options.Egl ?? new EglInterface(); var display = IntPtr.Zero; if (options.PlatformType == null) { @@ -30,9 +29,9 @@ static class EglDisplayUtils return display; } - public static EglConfigInfo InitializeAndGetConfig(EglInterface egl, IntPtr display, IEnumerable versions) + public static EglConfigInfo InitializeAndGetConfig(EglInterface egl, IntPtr display, IEnumerable? versions) { - if (!egl.Initialize(display, out var major, out var minor)) + if (!egl.Initialize(display, out _, out _)) throw OpenGlException.GetFormattedException("eglInitialize", egl); // TODO: AvaloniaLocator.Current.GetService()?.GlProfiles @@ -112,7 +111,7 @@ static class EglDisplayUtils } -class EglConfigInfo +internal class EglConfigInfo { public IntPtr Config { get; } public GlVersion Version { get; } diff --git a/src/Avalonia.OpenGL/Egl/EglGlPlatformSurface.cs b/src/Avalonia.OpenGL/Egl/EglGlPlatformSurface.cs index 348028bd08..59ef25b1a4 100644 --- a/src/Avalonia.OpenGL/Egl/EglGlPlatformSurface.cs +++ b/src/Avalonia.OpenGL/Egl/EglGlPlatformSurface.cs @@ -27,9 +27,9 @@ namespace Avalonia.OpenGL.Egl return new RenderTarget(glSurface, eglContext, _info); } - class RenderTarget : EglPlatformSurfaceRenderTargetBase + private class RenderTarget : EglPlatformSurfaceRenderTargetBase { - private EglSurface _glSurface; + private EglSurface? _glSurface; private readonly IEglWindowGlPlatformSurfaceInfo _info; private PixelSize _currentSize; private readonly IntPtr _handle; @@ -42,12 +42,12 @@ namespace Avalonia.OpenGL.Egl _handle = _info.Handle; } - public override void Dispose() => _glSurface.Dispose(); + public override void Dispose() => _glSurface?.Dispose(); public override IGlPlatformSurfaceRenderingSession BeginDrawCore() { if (_info.Size != _currentSize - || _handle != _info.Handle + || _handle != _info.Handle || _glSurface == null) { _glSurface?.Dispose(); diff --git a/src/Avalonia.OpenGL/Egl/EglGlPlatformSurfaceBase.cs b/src/Avalonia.OpenGL/Egl/EglGlPlatformSurfaceBase.cs index f66f630556..927e1253fc 100644 --- a/src/Avalonia.OpenGL/Egl/EglGlPlatformSurfaceBase.cs +++ b/src/Avalonia.OpenGL/Egl/EglGlPlatformSurfaceBase.cs @@ -33,7 +33,7 @@ namespace Avalonia.OpenGL.Egl public abstract IGlPlatformSurfaceRenderingSession BeginDrawCore(); protected IGlPlatformSurfaceRenderingSession BeginDraw(EglSurface surface, - PixelSize size, double scaling, Action onFinish = null, bool isYFlipped = false) + PixelSize size, double scaling, Action? onFinish = null, bool isYFlipped = false) { var restoreContext = Context.MakeCurrent(surface); @@ -56,19 +56,18 @@ namespace Avalonia.OpenGL.Egl restoreContext.Dispose(); } } - - class Session : IGlPlatformSurfaceRenderingSession + + private class Session : IGlPlatformSurfaceRenderingSession { private readonly EglContext _context; private readonly EglSurface _glSurface; private readonly EglDisplay _display; private readonly IDisposable _restoreContext; - private readonly Action _onFinish; - + private readonly Action? _onFinish; public Session(EglDisplay display, EglContext context, EglSurface glSurface, PixelSize size, double scaling, - IDisposable restoreContext, Action onFinish, bool isYFlipped) + IDisposable restoreContext, Action? onFinish, bool isYFlipped) { Size = size; Scaling = scaling; diff --git a/src/Avalonia.OpenGL/Egl/EglInterface.cs b/src/Avalonia.OpenGL/Egl/EglInterface.cs index d2d41f4def..2235d9087c 100644 --- a/src/Avalonia.OpenGL/Egl/EglInterface.cs +++ b/src/Avalonia.OpenGL/Egl/EglInterface.cs @@ -49,7 +49,7 @@ namespace Avalonia.OpenGL.Egl public partial IntPtr GetDisplay(IntPtr nativeDisplay); [GetProcAddress("eglGetPlatformDisplayEXT", true)] - public partial IntPtr GetPlatformDisplayExt(int platform, IntPtr nativeDisplay, int[] attrs); + public partial IntPtr GetPlatformDisplayExt(int platform, IntPtr nativeDisplay, int[]? attrs); [GetProcAddress("eglInitialize")] public partial bool Initialize(IntPtr display, out int major, out int minor); @@ -75,7 +75,7 @@ namespace Avalonia.OpenGL.Egl public partial bool DestroyContext(IntPtr display, IntPtr context); [GetProcAddress("eglCreatePbufferSurface")] - public partial IntPtr CreatePBufferSurface(IntPtr display, IntPtr config, int[] attrs); + public partial IntPtr CreatePBufferSurface(IntPtr display, IntPtr config, int[]? attrs); [GetProcAddress("eglMakeCurrent")] public partial bool MakeCurrent(IntPtr display, IntPtr draw, IntPtr read, IntPtr context); @@ -96,7 +96,7 @@ namespace Avalonia.OpenGL.Egl public partial void SwapBuffers(IntPtr display, IntPtr surface); [GetProcAddress("eglCreateWindowSurface")] - public partial IntPtr CreateWindowSurface(IntPtr display, IntPtr config, IntPtr window, int[] attrs); + public partial IntPtr CreateWindowSurface(IntPtr display, IntPtr config, IntPtr window, int[]? attrs); [GetProcAddress("eglBindTexImage")] public partial int BindTexImage(IntPtr display, IntPtr surface, int buffer); @@ -116,7 +116,7 @@ namespace Avalonia.OpenGL.Egl [GetProcAddress("eglQueryString")] public partial IntPtr QueryStringNative(IntPtr display, int i); - public string QueryString(IntPtr display, int i) + public string? QueryString(IntPtr display, int i) { var rv = QueryStringNative(display, i); if (rv == IntPtr.Zero) @@ -125,7 +125,7 @@ namespace Avalonia.OpenGL.Egl } [GetProcAddress("eglCreatePbufferFromClientBuffer")] - public partial IntPtr CreatePbufferFromClientBuffer(IntPtr display, int buftype, IntPtr buffer, IntPtr config, int[] attrib_list); + public partial IntPtr CreatePbufferFromClientBuffer(IntPtr display, int buftype, IntPtr buffer, IntPtr config, int[]? attrib_list); [GetProcAddress("eglQueryDisplayAttribEXT", true)] public partial bool QueryDisplayAttribExt(IntPtr display, int attr, out IntPtr res); diff --git a/src/Avalonia.OpenGL/Egl/EglPlatformGraphics.cs b/src/Avalonia.OpenGL/Egl/EglPlatformGraphics.cs index faa9f279a6..07e304febe 100644 --- a/src/Avalonia.OpenGL/Egl/EglPlatformGraphics.cs +++ b/src/Avalonia.OpenGL/Egl/EglPlatformGraphics.cs @@ -1,7 +1,6 @@ using System; using Avalonia.Logging; using Avalonia.Platform; -using static Avalonia.OpenGL.Egl.EglConsts; namespace Avalonia.OpenGL.Egl { @@ -24,7 +23,7 @@ namespace Avalonia.OpenGL.Egl AvaloniaLocator.CurrentMutable.Bind().ToConstant(feature); } - public static EglPlatformGraphics TryCreate() => TryCreate(() => new EglDisplay(new EglDisplayCreationOptions + public static EglPlatformGraphics? TryCreate() => TryCreate(() => new EglDisplay(new EglDisplayCreationOptions { Egl = new EglInterface(), // Those are expected to be supported by most EGL implementations @@ -32,7 +31,7 @@ namespace Avalonia.OpenGL.Egl SupportsContextSharing = true })); - public static EglPlatformGraphics TryCreate(Func displayFactory) + public static EglPlatformGraphics? TryCreate(Func displayFactory) { try { diff --git a/src/Avalonia.OpenGL/Features/ExternalObjectsOpenGlExtensionFeature.cs b/src/Avalonia.OpenGL/Features/ExternalObjectsOpenGlExtensionFeature.cs index 02d152ff61..6778726f84 100644 --- a/src/Avalonia.OpenGL/Features/ExternalObjectsOpenGlExtensionFeature.cs +++ b/src/Avalonia.OpenGL/Features/ExternalObjectsOpenGlExtensionFeature.cs @@ -63,10 +63,10 @@ public class ExternalObjectsOpenGlExtensionFeature : IGlContextExternalObjectsFe { private readonly IGlContext _context; private readonly ExternalObjectsInterface _ext; - private List _imageTypes = new(); - private List _semaphoreTypes = new(); + private readonly List _imageTypes = new(); + private readonly List _semaphoreTypes = new(); - public static ExternalObjectsOpenGlExtensionFeature TryCreate(IGlContext context) + public static ExternalObjectsOpenGlExtensionFeature? TryCreate(IGlContext context) { var extensions = context.GlInterface.GetExtensions(); if (extensions.Contains("GL_EXT_memory_object") && extensions.Contains("GL_EXT_semaphore")) @@ -142,10 +142,15 @@ public class ExternalObjectsOpenGlExtensionFeature : IGlContextExternalObjectsFe public IGlExternalImageTexture ImportImage(IPlatformHandle handle, PlatformGraphicsExternalImageProperties properties) { - if(!_imageTypes.Contains(handle.HandleDescriptor)) - throw new ArgumentException(handle.HandleDescriptor + " is not supported"); - - if (handle.HandleDescriptor == KnownPlatformGraphicsExternalImageHandleTypes.VulkanOpaquePosixFileDescriptor) + var handleDescriptor = handle.HandleDescriptor; + + if (string.IsNullOrEmpty(handleDescriptor)) + throw new ArgumentException("The handle must have a descriptor", nameof(handle)); + + if (!_imageTypes.Contains(handleDescriptor)) + throw new ArgumentException(handleDescriptor + " is not supported", nameof(handle)); + + if (handleDescriptor == KnownPlatformGraphicsExternalImageHandleTypes.VulkanOpaquePosixFileDescriptor) { while (_context.GlInterface.GetError() != 0) { @@ -174,23 +179,27 @@ public class ExternalObjectsOpenGlExtensionFeature : IGlContextExternalObjectsFe return new ExternalImageTexture(_context, properties, _ext, memoryObject, texture); } - throw new ArgumentException(handle.HandleDescriptor + " is not supported"); + throw new ArgumentException(handleDescriptor + " is not supported", nameof(handle)); } public IGlExternalSemaphore ImportSemaphore(IPlatformHandle handle) { - if(!_semaphoreTypes.Contains(handle.HandleDescriptor)) - throw new ArgumentException(handle.HandleDescriptor + " is not supported"); + var handleDescriptor = handle.HandleDescriptor; + + if (string.IsNullOrEmpty(handleDescriptor)) + throw new ArgumentException("The handle must have a descriptor", nameof(handle)); + + if (!_semaphoreTypes.Contains(handleDescriptor)) + throw new ArgumentException(handleDescriptor + " is not supported"); - if (handle.HandleDescriptor == - KnownPlatformGraphicsExternalSemaphoreHandleTypes.VulkanOpaquePosixFileDescriptor) + if (handleDescriptor == KnownPlatformGraphicsExternalSemaphoreHandleTypes.VulkanOpaquePosixFileDescriptor) { _ext.GenSemaphoresEXT(1, out var semaphore); _ext.ImportSemaphoreFdEXT(semaphore, GL_HANDLE_TYPE_OPAQUE_FD_EXT, handle.Handle.ToInt32()); return new ExternalSemaphore(_context, _ext, semaphore); } - throw new ArgumentException(handle.HandleDescriptor + " is not supported"); + throw new ArgumentException(handleDescriptor + " is not supported", nameof(handle)); } public CompositionGpuImportedImageSynchronizationCapabilities GetSynchronizationCapabilities(string imageHandleType) @@ -200,10 +209,10 @@ public class ExternalObjectsOpenGlExtensionFeature : IGlContextExternalObjectsFe return default; } - public byte[] DeviceLuid { get; } - public byte[] DeviceUuid { get; } + public byte[]? DeviceLuid { get; } + public byte[]? DeviceUuid { get; } - unsafe class ExternalSemaphore : IGlExternalSemaphore + private unsafe class ExternalSemaphore : IGlExternalSemaphore { private readonly IGlContext _context; private readonly ExternalObjectsInterface _ext; @@ -242,7 +251,7 @@ public class ExternalObjectsOpenGlExtensionFeature : IGlContextExternalObjectsFe } } - class ExternalImageTexture : IGlExternalImageTexture + private class ExternalImageTexture : IGlExternalImageTexture { private readonly IGlContext _context; private readonly ExternalObjectsInterface _ext; diff --git a/src/Avalonia.OpenGL/GlBasicInfoInterface.cs b/src/Avalonia.OpenGL/GlBasicInfoInterface.cs index 7a7110f15b..38969cc966 100644 --- a/src/Avalonia.OpenGL/GlBasicInfoInterface.cs +++ b/src/Avalonia.OpenGL/GlBasicInfoInterface.cs @@ -2,14 +2,14 @@ using System; using System.Collections.Generic; using System.Linq; using System.Runtime.InteropServices; -using Avalonia.Platform.Interop; using Avalonia.SourceGenerator; namespace Avalonia.OpenGL { public unsafe partial class GlBasicInfoInterface { - public GlBasicInfoInterface(Func getProcAddress){ + public GlBasicInfoInterface(Func getProcAddress) + { Initialize(getProcAddress); } @@ -22,7 +22,7 @@ namespace Avalonia.OpenGL [GetProcAddress("glGetStringi")] public partial IntPtr GetStringiNative(int v, int v1); - public string GetString(int v) + public string? GetString(int v) { var ptr = GetStringNative(v); if (ptr != IntPtr.Zero) @@ -30,7 +30,7 @@ namespace Avalonia.OpenGL return null; } - public string GetString(int v, int index) + public string? GetString(int v, int index) { var ptr = GetStringiNative(v, index); if (ptr != IntPtr.Zero) @@ -46,7 +46,13 @@ namespace Avalonia.OpenGL GetIntegerv(GlConsts.GL_NUM_EXTENSIONS, out int count); var rv = new List(count); for (var c = 0; c < count; c++) - rv.Add(GetString(GlConsts.GL_EXTENSIONS, c)); + { + if (GetString(GlConsts.GL_EXTENSIONS, c) is { } extension) + { + rv.Add(extension); + } + } + return rv; } } diff --git a/src/Avalonia.OpenGL/GlInterface.cs b/src/Avalonia.OpenGL/GlInterface.cs index a9dd882e3b..a6dadcd087 100644 --- a/src/Avalonia.OpenGL/GlInterface.cs +++ b/src/Avalonia.OpenGL/GlInterface.cs @@ -11,9 +11,9 @@ namespace Avalonia.OpenGL public unsafe partial class GlInterface : GlBasicInfoInterface { private readonly Func _getProcAddress; - public string Version { get; } - public string Vendor { get; } - public string Renderer { get; } + public string? Version { get; } + public string? Vendor { get; } + public string? Renderer { get; } public GlContextInfo ContextInfo { get; } public class GlContextInfo @@ -39,9 +39,9 @@ namespace Avalonia.OpenGL { _getProcAddress = getProcAddress; ContextInfo = info; - Version = GetString(GlConsts.GL_VERSION); - Renderer = GetString(GlConsts.GL_RENDERER); - Vendor = GetString(GlConsts.GL_VENDOR); + Version = GetString(GL_VERSION); + Renderer = GetString(GL_RENDERER); + Vendor = GetString(GL_VENDOR); Initialize(getProcAddress, ContextInfo); } @@ -203,7 +203,7 @@ namespace Avalonia.OpenGL [GetProcAddress("glGetShaderInfoLog")] public partial void GetShaderInfoLog(int shader, int maxLength, out int length, void* infoLog); - public unsafe string CompileShaderAndGetError(int shader, string source) + public unsafe string? CompileShaderAndGetError(int shader, string source) { ShaderSourceString(shader, source); CompileShader(shader); @@ -238,7 +238,7 @@ namespace Avalonia.OpenGL [GetProcAddress("glGetProgramInfoLog")] public partial void GetProgramInfoLog(int program, int maxLength, out int len, void* infoLog); - public unsafe string LinkProgramAndGetError(int program) + public unsafe string? LinkProgramAndGetError(int program) { LinkProgram(program); int compiled; @@ -374,4 +374,4 @@ namespace Avalonia.OpenGL }); } } -} \ No newline at end of file +} diff --git a/src/Avalonia.OpenGL/IGlContext.cs b/src/Avalonia.OpenGL/IGlContext.cs index d45ea931d0..fb4554d37f 100644 --- a/src/Avalonia.OpenGL/IGlContext.cs +++ b/src/Avalonia.OpenGL/IGlContext.cs @@ -15,7 +15,7 @@ namespace Avalonia.OpenGL IDisposable EnsureCurrent(); bool IsSharedWith(IGlContext context); bool CanCreateSharedContext { get; } - IGlContext CreateSharedContext(IEnumerable preferredVersions = null); + IGlContext? CreateSharedContext(IEnumerable? preferredVersions = null); } public interface IGlPlatformSurfaceRenderTargetFactory diff --git a/src/Avalonia.OpenGL/IOpenGlTextureSharingRenderInterfaceContextFeature.cs b/src/Avalonia.OpenGL/IOpenGlTextureSharingRenderInterfaceContextFeature.cs index 2043e944d1..19127fba40 100644 --- a/src/Avalonia.OpenGL/IOpenGlTextureSharingRenderInterfaceContextFeature.cs +++ b/src/Avalonia.OpenGL/IOpenGlTextureSharingRenderInterfaceContextFeature.cs @@ -1,4 +1,3 @@ -using System; using System.Collections.Generic; using Avalonia.Rendering.Composition; @@ -7,7 +6,7 @@ namespace Avalonia.OpenGL public interface IOpenGlTextureSharingRenderInterfaceContextFeature { bool CanCreateSharedContext { get; } - IGlContext CreateSharedContext(IEnumerable preferredVersions = null); + IGlContext? CreateSharedContext(IEnumerable? preferredVersions = null); ICompositionImportableOpenGlSharedTexture CreateSharedTextureForComposition(IGlContext context, PixelSize size); } diff --git a/src/Avalonia.OpenGL/OpenGlException.cs b/src/Avalonia.OpenGL/OpenGlException.cs index c498ed7833..d7a42c4400 100644 --- a/src/Avalonia.OpenGL/OpenGlException.cs +++ b/src/Avalonia.OpenGL/OpenGlException.cs @@ -5,13 +5,13 @@ namespace Avalonia.OpenGL { public class OpenGlException : Exception { - public int? ErrorCode { get; private set; } + public int? ErrorCode { get; } - public OpenGlException(string message) : base(message) + public OpenGlException(string? message) : base(message) { } - private OpenGlException(string message, int errorCode) : base(message) + private OpenGlException(string? message, int errorCode) : base(message) { ErrorCode = errorCode; } From 822f5d04aadf889eccb0ae12d16aa1f3b3708b05 Mon Sep 17 00:00:00 2001 From: Julien Lebosquain Date: Wed, 15 Feb 2023 20:00:31 +0100 Subject: [PATCH 40/49] Fix Direct2D1 test runner hanging --- .../Avalonia.Direct2D1/Direct2D1Platform.cs | 19 +++++++++++-------- 1 file changed, 11 insertions(+), 8 deletions(-) diff --git a/src/Windows/Avalonia.Direct2D1/Direct2D1Platform.cs b/src/Windows/Avalonia.Direct2D1/Direct2D1Platform.cs index 57379d1878..287db92b4d 100644 --- a/src/Windows/Avalonia.Direct2D1/Direct2D1Platform.cs +++ b/src/Windows/Avalonia.Direct2D1/Direct2D1Platform.cs @@ -1,7 +1,7 @@ using System; using System.Collections.Generic; +using System.Diagnostics; using System.IO; -using Avalonia.Controls; using Avalonia.Controls.Platform.Surfaces; using Avalonia.Direct2D1.Media; using Avalonia.Direct2D1.Media.Imaging; @@ -55,15 +55,18 @@ namespace Avalonia.Direct2D1 return; } #if DEBUG - try + if (Debugger.IsAttached) { - Direct2D1Factory = new SharpDX.Direct2D1.Factory1( - SharpDX.Direct2D1.FactoryType.MultiThreaded, + try + { + Direct2D1Factory = new SharpDX.Direct2D1.Factory1( + SharpDX.Direct2D1.FactoryType.MultiThreaded, SharpDX.Direct2D1.DebugLevel.Error); - } - catch - { - // + } + catch + { + // ignore, retry below without the debug layer + } } #endif if (Direct2D1Factory == null) From 9a5b351b50866261a058a10a29fb054b7c4c5c17 Mon Sep 17 00:00:00 2001 From: Max Katz Date: Thu, 16 Feb 2023 01:48:37 -0500 Subject: [PATCH 41/49] Fix build after global rename --- .../Platform/Storage/AndroidStorageProvider.cs | 6 +++--- .../Avalonia.Browser/Storage/BrowserStorageProvider.cs | 6 +++--- src/iOS/Avalonia.iOS/Storage/IOSStorageProvider.cs | 6 +++--- 3 files changed, 9 insertions(+), 9 deletions(-) diff --git a/src/Android/Avalonia.Android/Platform/Storage/AndroidStorageProvider.cs b/src/Android/Avalonia.Android/Platform/Storage/AndroidStorageProvider.cs index f611f50164..e35bde0acd 100644 --- a/src/Android/Avalonia.Android/Platform/Storage/AndroidStorageProvider.cs +++ b/src/Android/Avalonia.Android/Platform/Storage/AndroidStorageProvider.cs @@ -37,7 +37,7 @@ internal class AndroidStorageProvider : IStorageProvider return Task.FromResult(new AndroidStorageFolder(_activity, uri, false)); } - public async Task TryGetFileFromPath(Uri filePath) + public async Task TryGetFileFromPathAsync(Uri filePath) { if (filePath is null) { @@ -70,7 +70,7 @@ internal class AndroidStorageProvider : IStorageProvider return new AndroidStorageFile(_activity, androidUri); } - public async Task TryGetFolderFromPath(Uri folderPath) + public async Task TryGetFolderFromPathAsync(Uri folderPath) { if (folderPath is null) { @@ -103,7 +103,7 @@ internal class AndroidStorageProvider : IStorageProvider return new AndroidStorageFolder(_activity, androidUri, false); } - public Task TryGetWellKnownFolder(WellKnownFolder wellKnownFolder) + public Task TryGetWellKnownFolderAsync(WellKnownFolder wellKnownFolder) { var dirCode = wellKnownFolder switch { diff --git a/src/Browser/Avalonia.Browser/Storage/BrowserStorageProvider.cs b/src/Browser/Avalonia.Browser/Storage/BrowserStorageProvider.cs index 7497e82384..5b76d53a9d 100644 --- a/src/Browser/Avalonia.Browser/Storage/BrowserStorageProvider.cs +++ b/src/Browser/Avalonia.Browser/Storage/BrowserStorageProvider.cs @@ -116,17 +116,17 @@ internal class BrowserStorageProvider : IStorageProvider return item is not null ? new JSStorageFolder(item) : null; } - public Task TryGetFileFromPath(Uri filePath) + public Task TryGetFileFromPathAsync(Uri filePath) { return Task.FromResult(null); } - public Task TryGetFolderFromPath(Uri folderPath) + public Task TryGetFolderFromPathAsync(Uri folderPath) { return Task.FromResult(null); } - public async Task TryGetWellKnownFolder(WellKnownFolder wellKnownFolder) + public async Task TryGetWellKnownFolderAsync(WellKnownFolder wellKnownFolder) { await _lazyModule.Value; var directory = StorageHelper.CreateWellKnownDirectory(wellKnownFolder switch diff --git a/src/iOS/Avalonia.iOS/Storage/IOSStorageProvider.cs b/src/iOS/Avalonia.iOS/Storage/IOSStorageProvider.cs index ae2bf2a99d..9f27bfbbf5 100644 --- a/src/iOS/Avalonia.iOS/Storage/IOSStorageProvider.cs +++ b/src/iOS/Avalonia.iOS/Storage/IOSStorageProvider.cs @@ -104,19 +104,19 @@ internal class IOSStorageProvider : IStorageProvider ? new IOSStorageFolder(url) : null); } - public Task TryGetFileFromPath(Uri filePath) + public Task TryGetFileFromPathAsync(Uri filePath) { // TODO: research if it's possible, maybe with additional permissions. return Task.FromResult(null); } - public Task TryGetFolderFromPath(Uri folderPath) + public Task TryGetFolderFromPathAsync(Uri folderPath) { // TODO: research if it's possible, maybe with additional permissions. return Task.FromResult(null); } - public Task TryGetWellKnownFolder(WellKnownFolder wellKnownFolder) + public Task TryGetWellKnownFolderAsync(WellKnownFolder wellKnownFolder) { var directoryType = wellKnownFolder switch { From 95b6343f8f217f281f59127dff4928113d5bc94b Mon Sep 17 00:00:00 2001 From: Nikita Tsukanov Date: Thu, 16 Feb 2023 13:02:20 +0600 Subject: [PATCH 42/49] Respect AdornerLayer.IsClipEnabled --- .../Server/ServerCompositionVisual.cs | 5 +- src/Avalonia.Base/composition-schema.xml | 1 + .../Primitives/AdornerLayer.cs | 5 +- .../Controls/AdornerTests.cs | 81 +++++++++++------- ...s_Properly_Clipped_Clip_False.expected.png | Bin 0 -> 694 bytes ...s_Properly_Clipped_Clip_True.expected.png} | Bin ...s_Properly_Clipped_Clip_False.expected.png | Bin 0 -> 694 bytes ...s_Properly_Clipped_Clip_True.expected.png} | Bin 8 files changed, 56 insertions(+), 36 deletions(-) create mode 100644 tests/TestFiles/Direct2D1/Controls/Adorner/Focus_Adorner_Is_Properly_Clipped_Clip_False.expected.png rename tests/TestFiles/Direct2D1/Controls/Adorner/{Focus_Adorner_Is_Properly_Clipped.expected.png => Focus_Adorner_Is_Properly_Clipped_Clip_True.expected.png} (100%) create mode 100644 tests/TestFiles/Skia/Controls/Adorner/Focus_Adorner_Is_Properly_Clipped_Clip_False.expected.png rename tests/TestFiles/Skia/Controls/Adorner/{Focus_Adorner_Is_Properly_Clipped.expected.png => Focus_Adorner_Is_Properly_Clipped_Clip_True.expected.png} (100%) diff --git a/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionVisual.cs b/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionVisual.cs index e33dc999dc..98be861afa 100644 --- a/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionVisual.cs +++ b/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionVisual.cs @@ -48,7 +48,8 @@ namespace Avalonia.Rendering.Composition.Server { canvas.PostTransform = Matrix.Identity; canvas.Transform = Matrix.Identity; - canvas.PushClip(AdornedVisual._combinedTransformedClipBounds); + if (AdornerIsClipped) + canvas.PushClip(AdornedVisual._combinedTransformedClipBounds); } var transform = GlobalTransformMatrix; canvas.PostTransform = MatrixUtils.ToMatrix(transform); @@ -74,7 +75,7 @@ namespace Avalonia.Rendering.Composition.Server canvas.PopGeometryClip(); if (ClipToBounds && !HandlesClipToBounds) canvas.PopClip(); - if (AdornedVisual != null) + if (AdornedVisual != null && AdornerIsClipped) canvas.PopClip(); if(Opacity != 1) canvas.PopOpacity(); diff --git a/src/Avalonia.Base/composition-schema.xml b/src/Avalonia.Base/composition-schema.xml index 36fd9fe709..31722974ee 100644 --- a/src/Avalonia.Base/composition-schema.xml +++ b/src/Avalonia.Base/composition-schema.xml @@ -26,6 +26,7 @@ + diff --git a/src/Avalonia.Controls/Primitives/AdornerLayer.cs b/src/Avalonia.Controls/Primitives/AdornerLayer.cs index 79719912ea..611d57a980 100644 --- a/src/Avalonia.Controls/Primitives/AdornerLayer.cs +++ b/src/Avalonia.Controls/Primitives/AdornerLayer.cs @@ -279,8 +279,11 @@ namespace Avalonia.Controls.Primitives private void UpdateAdornedElement(Visual adorner, Visual? adorned) { if (adorner.CompositionVisual != null) + { adorner.CompositionVisual.AdornedVisual = adorned?.CompositionVisual; - + adorner.CompositionVisual.AdornerIsClipped = GetIsClipEnabled(adorner); + } + var info = adorner.GetValue(s_adornedElementInfoProperty); if (info != null) diff --git a/tests/Avalonia.RenderTests/Controls/AdornerTests.cs b/tests/Avalonia.RenderTests/Controls/AdornerTests.cs index c0159aecff..b158bf798d 100644 --- a/tests/Avalonia.RenderTests/Controls/AdornerTests.cs +++ b/tests/Avalonia.RenderTests/Controls/AdornerTests.cs @@ -1,3 +1,4 @@ +using System.Runtime.CompilerServices; using System.Threading.Tasks; using Avalonia.Controls; using Avalonia.Controls.Primitives; @@ -18,56 +19,70 @@ public class AdornerTests : TestBase { } - [Fact] - public async Task Focus_Adorner_Is_Properly_Clipped() + async Task CheckAdornedContent(Control content, Control adorned, Control adorner, int width = 200, int height = 200, + [CallerMemberName] string testName = "") { - Border adorned; var tree = new Decorator { Child = new VisualLayerManager { - Child = new Border - { - Background = Brushes.Red, - Padding = new Thickness(10, 50, 10,10), - Child = new Border() - { - Background = Brushes.White, - ClipToBounds = true, - Padding = new Thickness(0, -30, 0, 0), - Child = adorned = new Border - { - Background = Brushes.Green, - VerticalAlignment = VerticalAlignment.Top, - Height = 100, - Width = 50 - } - } - } + Child = content }, - Width = 200, - Height = 200 - }; - var adorner = new Border - { - BorderThickness = new Thickness(2), - BorderBrush = Brushes.Black + Width = width, + Height = height }; - + var size = new Size(tree.Width, tree.Height); tree.Measure(size); tree.Arrange(new Rect(size)); - - + adorned.AttachedToVisualTree += delegate { AdornerLayer.SetAdornedElement(adorner, adorned); AdornerLayer.GetAdornerLayer(adorned)!.Children.Add(adorner); }; + tree.Measure(size); tree.Arrange(new Rect(size)); - await RenderToFile(tree); - CompareImages(skipImmediate: true); + await RenderToFile(tree, testName: testName); + CompareImages(skipImmediate: true, testName: testName); + } + + [Theory, + InlineData(true), + InlineData(false) + ] + public async Task Focus_Adorner_Is_Properly_Clipped(bool clip) + { + Border adorned; + var content = new Border + { + Background = Brushes.Red, + Padding = new Thickness(10, 50, 10, 10), + Child = new Border() + { + Background = Brushes.White, + ClipToBounds = true, + Padding = new Thickness(0, -30, 0, 0), + Child = adorned = new Border + { + Background = Brushes.Green, + VerticalAlignment = VerticalAlignment.Top, + Height = 100, + Width = 50 + } + } + }; + var adorner = new Border + { + BorderThickness = new Thickness(2), + BorderBrush = Brushes.Black + }; + if (!clip) + AdornerLayer.SetIsClipEnabled(adorner, false); + await CheckAdornedContent(content, adorned, adorner, + testName: "Focus_Adorner_Is_Properly_Clipped_Clip_" + clip); } + } \ No newline at end of file diff --git a/tests/TestFiles/Direct2D1/Controls/Adorner/Focus_Adorner_Is_Properly_Clipped_Clip_False.expected.png b/tests/TestFiles/Direct2D1/Controls/Adorner/Focus_Adorner_Is_Properly_Clipped_Clip_False.expected.png new file mode 100644 index 0000000000000000000000000000000000000000..4821c22c397099cdae149b5ed9cc367ab9844a66 GIT binary patch literal 694 zcmeAS@N?(olHy`uVBq!ia0vp^CqS5k4M?tyST_$yu@pObhHwBu4M$1`0|Qg8r;B4q z#hkZyHf9|*5OD}x`Q-5D=dUhYT6t$y!XgXX^pB4oHtwjJ{Y)yE_d&S)ZuW+7H31H$ zMg<~?g4?zSrk=UWJm-iq%ki5E0yz`dW4^N0P3QgLW5&VK=paB5(egO|^SaghJgOJ{ z{JQJyp7*kk6TiP-zxj{l>HRkp1X!3H2R{J}SlM62vcKQXJ!@IsU(oeb=eR}1G&7Et z+wU0H9Xb2^|4{J=IJAMjc`L^hHHRkp1X!3H2R{J}SlM62vcKQXJ!@IsU(oeb=eR}1G&7Et z+wU0H9Xb2^|4{J=IJAMjc`L^hH Date: Thu, 16 Feb 2023 10:14:01 +0100 Subject: [PATCH 43/49] Update Measure and Layout if a child of RelativePanel updates it's position. --- .../RelativePanel.AttachedProperties.cs | 39 ++++++++----------- 1 file changed, 16 insertions(+), 23 deletions(-) diff --git a/src/Avalonia.Controls/RelativePanel.AttachedProperties.cs b/src/Avalonia.Controls/RelativePanel.AttachedProperties.cs index 18cf96ddca..d2b91def7d 100644 --- a/src/Avalonia.Controls/RelativePanel.AttachedProperties.cs +++ b/src/Avalonia.Controls/RelativePanel.AttachedProperties.cs @@ -1,37 +1,30 @@ using Avalonia.Layout; +using Avalonia.Threading; namespace Avalonia.Controls { public partial class RelativePanel { - private static void OnAlignPropertiesChanged(AvaloniaObject d, AvaloniaPropertyChangedEventArgs e) - { - if (d is Layoutable layoutable && layoutable.Parent is Layoutable layoutableParent) - { - layoutableParent.InvalidateArrange(); - } - } static RelativePanel() { ClipToBoundsProperty.OverrideDefaultValue(true); - AboveProperty.Changed.AddClassHandler(OnAlignPropertiesChanged); - AlignBottomWithPanelProperty.Changed.AddClassHandler(OnAlignPropertiesChanged); - AlignBottomWithProperty.Changed.AddClassHandler(OnAlignPropertiesChanged); - AlignHorizontalCenterWithPanelProperty.Changed.AddClassHandler(OnAlignPropertiesChanged); - AlignHorizontalCenterWithProperty.Changed.AddClassHandler(OnAlignPropertiesChanged); - AlignLeftWithPanelProperty.Changed.AddClassHandler(OnAlignPropertiesChanged); - AlignLeftWithProperty.Changed.AddClassHandler(OnAlignPropertiesChanged); - AlignRightWithPanelProperty.Changed.AddClassHandler(OnAlignPropertiesChanged); - AlignRightWithProperty.Changed.AddClassHandler(OnAlignPropertiesChanged); - AlignTopWithPanelProperty.Changed.AddClassHandler(OnAlignPropertiesChanged); - AlignTopWithProperty.Changed.AddClassHandler(OnAlignPropertiesChanged); - AlignVerticalCenterWithPanelProperty.Changed.AddClassHandler(OnAlignPropertiesChanged); - AlignVerticalCenterWithProperty.Changed.AddClassHandler(OnAlignPropertiesChanged); - BelowProperty.Changed.AddClassHandler(OnAlignPropertiesChanged); - LeftOfProperty.Changed.AddClassHandler(OnAlignPropertiesChanged); - RightOfProperty.Changed.AddClassHandler(OnAlignPropertiesChanged); + AffectsParentArrange( + AlignLeftWithPanelProperty, AlignLeftWithProperty, LeftOfProperty, + AlignRightWithPanelProperty, AlignRightWithProperty, RightOfProperty, + AlignTopWithPanelProperty, AlignTopWithProperty, AboveProperty, + AlignBottomWithPanelProperty, AlignBottomWithProperty, BelowProperty, + AlignHorizontalCenterWithPanelProperty, AlignHorizontalCenterWithProperty, + AlignVerticalCenterWithPanelProperty, AlignVerticalCenterWithProperty); + + AffectsParentMeasure( + AlignLeftWithPanelProperty, AlignLeftWithProperty, LeftOfProperty, + AlignRightWithPanelProperty, AlignRightWithProperty, RightOfProperty, + AlignTopWithPanelProperty, AlignTopWithProperty, AboveProperty, + AlignBottomWithPanelProperty, AlignBottomWithProperty, BelowProperty, + AlignHorizontalCenterWithPanelProperty, AlignHorizontalCenterWithProperty, + AlignVerticalCenterWithPanelProperty, AlignVerticalCenterWithProperty); } /// From 8280b83e95d1f6803194a27bd3c601cf120b21de Mon Sep 17 00:00:00 2001 From: Nikita Tsukanov Date: Thu, 16 Feb 2023 15:22:20 +0600 Subject: [PATCH 44/49] Automatically dispatch PropertyChanged notification to the UI thread --- .../Data/Core/IndexerNodeBase.cs | 4 ++-- .../Plugins/InpcPropertyAccessorPlugin.cs | 4 ++-- src/Avalonia.Base/Utilities/WeakEvents.cs | 24 +++++++++++++++---- .../PropertyInfoAccessorFactory.cs | 4 ++-- 4 files changed, 26 insertions(+), 10 deletions(-) diff --git a/src/Avalonia.Base/Data/Core/IndexerNodeBase.cs b/src/Avalonia.Base/Data/Core/IndexerNodeBase.cs index 9ec256225b..57e4fa4a8e 100644 --- a/src/Avalonia.Base/Data/Core/IndexerNodeBase.cs +++ b/src/Avalonia.Base/Data/Core/IndexerNodeBase.cs @@ -22,7 +22,7 @@ namespace Avalonia.Data.Core if (target is INotifyPropertyChanged inpc) { - WeakEvents.PropertyChanged.Subscribe(inpc, this); + WeakEvents.ThreadSafePropertyChanged.Subscribe(inpc, this); } ValueChanged(GetValue(target)); @@ -39,7 +39,7 @@ namespace Avalonia.Data.Core if (target is INotifyPropertyChanged inpc) { - WeakEvents.PropertyChanged.Unsubscribe(inpc, this); + WeakEvents.ThreadSafePropertyChanged.Unsubscribe(inpc, this); } } } diff --git a/src/Avalonia.Base/Data/Core/Plugins/InpcPropertyAccessorPlugin.cs b/src/Avalonia.Base/Data/Core/Plugins/InpcPropertyAccessorPlugin.cs index 7c2caf02b4..e8e3e6d509 100644 --- a/src/Avalonia.Base/Data/Core/Plugins/InpcPropertyAccessorPlugin.cs +++ b/src/Avalonia.Base/Data/Core/Plugins/InpcPropertyAccessorPlugin.cs @@ -160,7 +160,7 @@ namespace Avalonia.Data.Core.Plugins var inpc = GetReferenceTarget() as INotifyPropertyChanged; if (inpc != null) - WeakEvents.PropertyChanged.Unsubscribe(inpc, this); + WeakEvents.ThreadSafePropertyChanged.Unsubscribe(inpc, this); } private object? GetReferenceTarget() @@ -185,7 +185,7 @@ namespace Avalonia.Data.Core.Plugins var inpc = GetReferenceTarget() as INotifyPropertyChanged; if (inpc != null) - WeakEvents.PropertyChanged.Subscribe(inpc, this); + WeakEvents.ThreadSafePropertyChanged.Subscribe(inpc, this); } } } diff --git a/src/Avalonia.Base/Utilities/WeakEvents.cs b/src/Avalonia.Base/Utilities/WeakEvents.cs index 6da899bab2..2f62564e0e 100644 --- a/src/Avalonia.Base/Utilities/WeakEvents.cs +++ b/src/Avalonia.Base/Utilities/WeakEvents.cs @@ -2,6 +2,7 @@ using System; using System.Collections.Specialized; using System.ComponentModel; using System.Windows.Input; +using Avalonia.Threading; namespace Avalonia.Utilities; @@ -20,15 +21,30 @@ public class WeakEvents }); /// - /// Represents PropertyChanged event from + /// Represents PropertyChanged event from with auto-dispatching to the UI thread /// public static readonly WeakEvent - PropertyChanged = WeakEvent.Register( + ThreadSafePropertyChanged = WeakEvent.Register( (s, h) => { - PropertyChangedEventHandler handler = (_, e) => h(s, e); + bool unsubscribed = false; + PropertyChangedEventHandler handler = (_, e) => + { + if (Dispatcher.UIThread.CheckAccess()) + h(s, e); + else + Dispatcher.UIThread.Post(() => + { + if (!unsubscribed) + h(s, e); + }); + }; s.PropertyChanged += handler; - return () => s.PropertyChanged -= handler; + return () => + { + unsubscribed = true; + s.PropertyChanged -= handler; + }; }); diff --git a/src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/CompiledBindings/PropertyInfoAccessorFactory.cs b/src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/CompiledBindings/PropertyInfoAccessorFactory.cs index abb166a92b..9e58f81b55 100644 --- a/src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/CompiledBindings/PropertyInfoAccessorFactory.cs +++ b/src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/CompiledBindings/PropertyInfoAccessorFactory.cs @@ -121,7 +121,7 @@ namespace Avalonia.Markup.Xaml.MarkupExtensions.CompiledBindings { if (_reference.TryGetTarget(out var o) && o is INotifyPropertyChanged inpc) { - WeakEvents.PropertyChanged.Unsubscribe(inpc, this); + WeakEvents.ThreadSafePropertyChanged.Unsubscribe(inpc, this); } } @@ -138,7 +138,7 @@ namespace Avalonia.Markup.Xaml.MarkupExtensions.CompiledBindings private void SubscribeToChanges() { if (_reference.TryGetTarget(out var o) && o is INotifyPropertyChanged inpc) - WeakEvents.PropertyChanged.Subscribe(inpc, this); + WeakEvents.ThreadSafePropertyChanged.Subscribe(inpc, this); } } From 80dc4dd0b7bc03ce957e3f7e7d25cae201b18a5d Mon Sep 17 00:00:00 2001 From: Giuseppe Lippolis Date: Sat, 21 Jan 2023 11:42:32 +0100 Subject: [PATCH 45/49] fix: iOS Nullable --- src/iOS/Avalonia.iOS/PlatformSettings.cs | 5 +- .../Avalonia.iOS/Storage/IOSStorageItem.cs | 8 +- src/iOS/Avalonia.iOS/TextInputResponder.cs | 81 +++++++++++++------ 3 files changed, 62 insertions(+), 32 deletions(-) diff --git a/src/iOS/Avalonia.iOS/PlatformSettings.cs b/src/iOS/Avalonia.iOS/PlatformSettings.cs index 0ae290468b..082966f5b2 100644 --- a/src/iOS/Avalonia.iOS/PlatformSettings.cs +++ b/src/iOS/Avalonia.iOS/PlatformSettings.cs @@ -1,8 +1,7 @@ +#nullable enable using System; -using System.Linq; using Avalonia.Media; using Avalonia.Platform; -using Foundation; using UIKit; namespace Avalonia.iOS; @@ -10,7 +9,7 @@ namespace Avalonia.iOS; // TODO: ideally should be created per view/activity. internal class PlatformSettings : DefaultPlatformSettings { - private PlatformColorValues _lastColorValues; + private PlatformColorValues? _lastColorValues; public override PlatformColorValues GetColorValues() { diff --git a/src/iOS/Avalonia.iOS/Storage/IOSStorageItem.cs b/src/iOS/Avalonia.iOS/Storage/IOSStorageItem.cs index 6fa65f1265..27bd8faf64 100644 --- a/src/iOS/Avalonia.iOS/Storage/IOSStorageItem.cs +++ b/src/iOS/Avalonia.iOS/Storage/IOSStorageItem.cs @@ -1,6 +1,5 @@ using System; using System.Collections.Generic; -using System.Diagnostics.CodeAnalysis; using System.IO; using System.Linq; using System.Threading.Tasks; @@ -24,8 +23,11 @@ internal abstract class IOSStorageItem : IStorageBookmarkItem using (var doc = new UIDocument(url)) { - _filePath = doc.FileUrl?.Path ?? url.FilePathUrl.Path; - Name = doc.LocalizedName ?? System.IO.Path.GetFileName(_filePath) ?? url.FilePathUrl.LastPathComponent; + _filePath = doc.FileUrl?.Path ?? url.FilePathUrl?.Path ?? string.Empty; + Name = doc.LocalizedName + ?? System.IO.Path.GetFileName(_filePath) + ?? url.FilePathUrl?.LastPathComponent + ?? string.Empty; } } diff --git a/src/iOS/Avalonia.iOS/TextInputResponder.cs b/src/iOS/Avalonia.iOS/TextInputResponder.cs index 8862f4e392..e444a90506 100644 --- a/src/iOS/Avalonia.iOS/TextInputResponder.cs +++ b/src/iOS/Avalonia.iOS/TextInputResponder.cs @@ -1,14 +1,13 @@ using System; using System.Runtime.InteropServices; -using System.Runtime.Versioning; using Avalonia.Controls.Presenters; -using Foundation; -using ObjCRuntime; -using Avalonia.Input.TextInput; using Avalonia.Input; using Avalonia.Input.Raw; +using Avalonia.Input.TextInput; using Avalonia.Logging; using CoreGraphics; +using Foundation; +using ObjCRuntime; using UIKit; // ReSharper disable InconsistentNaming // ReSharper disable StringLiteralTypo @@ -25,6 +24,9 @@ partial class AvaloniaView [Adopts("UIKeyInput")] partial class TextInputResponder : UIResponder, IUITextInput { + private static AvaloniaEmptyTextPosition? _emptyPosition; + private static AvaloniaEmptyTextPosition EmptyPosition => _emptyPosition ??= new(); + private class AvaloniaTextRange : UITextRange, INSCopying { private UITextPosition? _start; @@ -67,6 +69,15 @@ partial class AvaloniaView public NSObject Copy(NSZone? zone) => new AvaloniaTextPosition(Index); } + private class AvaloniaEmptyTextPosition : UITextPosition, INSCopying + { + public AvaloniaEmptyTextPosition() + { + + } + public NSObject Copy(NSZone? zone) => this; + } + public TextInputResponder(AvaloniaView view, ITextInputMethodClient client) { _view = view; @@ -93,7 +104,25 @@ partial class AvaloniaView public override NSString TextInputContextIdentifier => new NSString(Guid.NewGuid().ToString()); - public override UITextInputMode TextInputMode => UITextInputMode.CurrentInputMode; + public override UITextInputMode TextInputMode + { + get + { + var mode = UITextInputMode.CurrentInputMode; + // Can be empty see https://developer.apple.com/documentation/uikit/uitextinputmode/1614522-activeinputmodes + if (mode is null && UITextInputMode.ActiveInputModes.Length > 0) + { + mode = UITextInputMode.ActiveInputModes[0]; + } + // See: https://stackoverflow.com/a/33337483/20894223 + if (mode is null) + { + using var tv = new UITextView(); + mode = tv.TextInputMode; + } + return mode; + } + } [DllImport("/usr/lib/libobjc.dylib")] private static extern void objc_msgSend(IntPtr receiver, IntPtr selector, IntPtr arg); @@ -105,8 +134,8 @@ partial class AvaloniaView private readonly AvaloniaView _view; private string? _markedText; - - + + private void SurroundingTextChanged(object? sender, EventArgs e) { Logger.TryGet(LogEventLevel.Debug, ImeLog)?.Log(null, "SurroundingTextChanged"); @@ -153,9 +182,9 @@ partial class AvaloniaView switch (ReturnKeyType) { case UIReturnKeyType.Done: - case UIReturnKeyType.Go: - case UIReturnKeyType.Send: - case UIReturnKeyType.Search: + case UIReturnKeyType.Go: + case UIReturnKeyType.Send: + case UIReturnKeyType.Search: ResignFirstResponder(); break; } @@ -164,7 +193,7 @@ partial class AvaloniaView TextInput(text); } - + void IUIKeyInput.DeleteBackward() => KeyPress(Key.Back); bool IUIKeyInput.HasText => true; @@ -176,8 +205,8 @@ partial class AvaloniaView Logger.TryGet(LogEventLevel.Debug, ImeLog)?.Log(null, "IUIKeyInput.TextInRange {start} {end}", r.StartIndex, r.EndIndex); string result = ""; - if(string.IsNullOrEmpty(_markedText)) - result = s.Text[r.StartIndex .. r.EndIndex]; + if (string.IsNullOrEmpty(_markedText)) + result = s.Text[r.StartIndex..r.EndIndex]; else { var span = new CombinedSpan3(s.Text.AsSpan().Slice(0, s.CursorOffset), @@ -214,7 +243,7 @@ partial class AvaloniaView void IUITextInput.UnmarkText() { Logger.TryGet(LogEventLevel.Debug, ImeLog)?.Log(null, "IUIKeyInput.UnmarkText"); - if(_markedText == null) + if (_markedText == null) return; var commitString = _markedText; _markedText = null; @@ -239,15 +268,15 @@ partial class AvaloniaView Logger.TryGet(LogEventLevel.Debug, ImeLog) ?.Log(null, "IUIKeyInput.GetPosition {start} {offset}", pos.Index, (int)offset); - var res = GetPositionCore(pos, offset); - Logger.TryGet(LogEventLevel.Debug, ImeLog) - ?.Log(null, $"res: " + (res == null ? "null" : (int)res.Index)); - return res!; + var res = GetPositionCore(pos, offset); + Logger.TryGet(LogEventLevel.Debug, ImeLog) + ?.Log(null, $"res: " + (res == null ? "null" : (int)res.Index)); + return res!; } private AvaloniaTextPosition? GetPositionCore(AvaloniaTextPosition pos, nint offset) { - + var end = pos.Index + (int)offset; if (end < 0) return null!; @@ -261,14 +290,14 @@ partial class AvaloniaView { var pos = (AvaloniaTextPosition)fromPosition; Logger.TryGet(LogEventLevel.Debug, ImeLog) - ?.Log(null, "IUIKeyInput.GetPosition {start} {direction} {offset}", pos.Index, inDirection, (int)offset); + ?.Log(null, "IUIKeyInput.GetPosition {start} {direction} {offset}", pos.Index, inDirection, (int)offset); var res = GetPositionCore(pos, inDirection, offset); Logger.TryGet(LogEventLevel.Debug, ImeLog) ?.Log(null, $"res: " + (res == null ? "null" : (int)res.Index)); return res!; } - + private AvaloniaTextPosition? GetPositionCore(AvaloniaTextPosition fromPosition, UITextLayoutDirection inDirection, nint offset) { @@ -348,7 +377,7 @@ partial class AvaloniaView CGRect IUITextInput.GetFirstRectForRange(UITextRange range) { - + Logger.TryGet(LogEventLevel.Debug, ImeLog)? .Log(null, "IUITextInput:GetFirstRectForRange"); // TODO: Query from the input client @@ -377,11 +406,11 @@ partial class AvaloniaView if (presenter is { }) { var hitResult = presenter.TextLayout.HitTestPoint(new Point(point.X, point.Y)); - + return new AvaloniaTextPosition(hitResult.TextPosition); } - return null; + return EmptyPosition; } UITextPosition IUITextInput.GetClosestPositionToPoint(CGPoint point, UITextRange withinRange) @@ -440,7 +469,7 @@ partial class AvaloniaView NSDictionary? IUITextInput.MarkedTextStyle { get => null; - set {} + set { } } UITextPosition IUITextInput.BeginningOfDocument => _beginningOfDocument; @@ -478,7 +507,7 @@ partial class AvaloniaView var res = base.ResignFirstResponder(); if (res && ReferenceEquals(CurrentAvaloniaResponder, this)) { - + Logger.TryGet(LogEventLevel.Debug, "IOSIME") ?.Log(null, "Resigned first responder"); _client.SurroundingTextChanged -= SurroundingTextChanged; From 81f6f65c263a5e6fa27c9df3ef75c0515762818d Mon Sep 17 00:00:00 2001 From: Dariusz Komosinski Date: Thu, 16 Feb 2023 11:34:37 +0100 Subject: [PATCH 46/49] Implement LayoutInformation. --- src/Avalonia.Base/Layout/LayoutInformation.cs | 27 +++++++++++++++++++ src/Avalonia.Base/Layout/Layoutable.cs | 4 +-- 2 files changed, 29 insertions(+), 2 deletions(-) create mode 100644 src/Avalonia.Base/Layout/LayoutInformation.cs diff --git a/src/Avalonia.Base/Layout/LayoutInformation.cs b/src/Avalonia.Base/Layout/LayoutInformation.cs new file mode 100644 index 0000000000..9b821053a2 --- /dev/null +++ b/src/Avalonia.Base/Layout/LayoutInformation.cs @@ -0,0 +1,27 @@ +namespace Avalonia.Layout; + +/// +/// Provides access to layout information of a control. +/// +public static class LayoutInformation +{ + /// + /// Gets the available size constraint passed in the previous layout pass. + /// + /// The control. + /// Previous control measure constraint, if any. + public static Size? GetPreviousMeasureConstraint(Layoutable control) + { + return control.PreviousMeasure; + } + + /// + /// Gets the control bounds used in the previous layout arrange pass. + /// + /// The control. + /// Previous control arrange bounds, if any. + public static Rect? GetPreviousArrangeBounds(Layoutable control) + { + return control.PreviousArrange; + } +} diff --git a/src/Avalonia.Base/Layout/Layoutable.cs b/src/Avalonia.Base/Layout/Layoutable.cs index f14ad3058a..4a273b0291 100644 --- a/src/Avalonia.Base/Layout/Layoutable.cs +++ b/src/Avalonia.Base/Layout/Layoutable.cs @@ -326,12 +326,12 @@ namespace Avalonia.Layout /// /// Gets the available size passed in the previous layout pass, if any. /// - public Size? PreviousMeasure => _previousMeasure; + internal Size? PreviousMeasure => _previousMeasure; /// /// Gets the layout rect passed in the previous layout pass, if any. /// - public Rect? PreviousArrange => _previousArrange; + internal Rect? PreviousArrange => _previousArrange; /// /// Creates the visual children of the control, if necessary From 4c36520ed6a9b49893e2115bec42da540d44d1de Mon Sep 17 00:00:00 2001 From: Dariusz Komosinski Date: Thu, 16 Feb 2023 17:35:02 +0100 Subject: [PATCH 47/49] Fix alignment helper not working and missing icon for clear button. --- .../Diagnostics/Controls/FilterTextBox.axaml | 2 ++ .../Diagnostics/ViewModels/ControlLayoutViewModel.cs | 4 ++-- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/src/Avalonia.Diagnostics/Diagnostics/Controls/FilterTextBox.axaml b/src/Avalonia.Diagnostics/Diagnostics/Controls/FilterTextBox.axaml index 3bfe511fbc..1b5f431f36 100644 --- a/src/Avalonia.Diagnostics/Diagnostics/Controls/FilterTextBox.axaml +++ b/src/Avalonia.Diagnostics/Diagnostics/Controls/FilterTextBox.axaml @@ -16,6 +16,8 @@