From 571b7746eac6cc694aa586ec83d48ad5c9f4b9ac Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Sun, 22 Jan 2023 11:46:28 +0100 Subject: [PATCH 01/70] 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/70] 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/70] 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/70] 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/70] 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/70] 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/70] 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/70] 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/70] 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/70] 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/70] 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/70] 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/70] 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/70] 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/70] 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/70] 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/70] 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/70] 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/70] 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/70] 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 19078979e38a9facb0be00ed0daeca3bd53c9e3a Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Mon, 13 Feb 2023 09:40:42 +0100 Subject: [PATCH 21/70] Initial implementation of SetCurrentValue. --- src/Avalonia.Base/AvaloniaObject.cs | 20 +- .../Diagnostics/AvaloniaPropertyValue.cs | 23 +- .../PropertyStore/EffectiveValue.cs | 6 + .../PropertyStore/EffectiveValue`1.cs | 29 +- src/Avalonia.Base/PropertyStore/ValueStore.cs | 23 +- .../AvaloniaObjectTests_SetCurrentValue.cs | 270 ++++++++++++++++++ 6 files changed, 345 insertions(+), 26 deletions(-) create mode 100644 tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_SetCurrentValue.cs diff --git a/src/Avalonia.Base/AvaloniaObject.cs b/src/Avalonia.Base/AvaloniaObject.cs index 74dc55355b..93bbee12b8 100644 --- a/src/Avalonia.Base/AvaloniaObject.cs +++ b/src/Avalonia.Base/AvaloniaObject.cs @@ -355,6 +355,23 @@ namespace Avalonia SetDirectValueUnchecked(property, value); } + public void SetCurrentValue(StyledProperty property, T value) + { + _ = property ?? throw new ArgumentNullException(nameof(property)); + VerifyAccess(); + + LogPropertySet(property, value, BindingPriority.LocalValue); + + if (value is UnsetValueType) + { + _values.ClearLocalValue(property); + } + else if (value is not DoNothingType) + { + _values.SetCurrentValue(property, value); + } + } + /// /// Binds a to an observable. /// @@ -547,7 +564,8 @@ namespace Avalonia property, GetValue(property), BindingPriority.LocalValue, - null); + null, + false); } return _values.GetDiagnostic(property); diff --git a/src/Avalonia.Base/Diagnostics/AvaloniaPropertyValue.cs b/src/Avalonia.Base/Diagnostics/AvaloniaPropertyValue.cs index 4189fd5234..0b3e62f1cc 100644 --- a/src/Avalonia.Base/Diagnostics/AvaloniaPropertyValue.cs +++ b/src/Avalonia.Base/Diagnostics/AvaloniaPropertyValue.cs @@ -3,28 +3,23 @@ using Avalonia.Data; namespace Avalonia.Diagnostics { /// - /// Holds diagnostic-related information about the value of a - /// on a . + /// Holds diagnostic-related information about the value of an + /// on an . /// public class AvaloniaPropertyValue { - /// - /// Initializes a new instance of the class. - /// - /// The property. - /// The current property value. - /// The priority of the current value. - /// A diagnostic string. - public AvaloniaPropertyValue( + internal AvaloniaPropertyValue( AvaloniaProperty property, object? value, BindingPriority priority, - string? diagnostic) + string? diagnostic, + bool isOverriddenCurrentValue) { Property = property; Value = value; Priority = priority; Diagnostic = diagnostic; + IsOverriddenCurrentValue = isOverriddenCurrentValue; } /// @@ -46,5 +41,11 @@ namespace Avalonia.Diagnostics /// Gets a diagnostic string. /// public string? Diagnostic { get; } + + /// + /// Gets a value indicating whether the was overridden by a call to + /// . + /// + public bool IsOverriddenCurrentValue { get; } } } diff --git a/src/Avalonia.Base/PropertyStore/EffectiveValue.cs b/src/Avalonia.Base/PropertyStore/EffectiveValue.cs index 04d3c805c2..78f0ad46b7 100644 --- a/src/Avalonia.Base/PropertyStore/EffectiveValue.cs +++ b/src/Avalonia.Base/PropertyStore/EffectiveValue.cs @@ -29,6 +29,12 @@ namespace Avalonia.PropertyStore /// public BindingPriority BasePriority { get; protected set; } + /// + /// Gets a value indicating whether the was overridden by a call to + /// . + /// + public bool IsOverridenCurrentValue { get; set; } + /// /// Begins a reevaluation pass on the effective value. /// diff --git a/src/Avalonia.Base/PropertyStore/EffectiveValue`1.cs b/src/Avalonia.Base/PropertyStore/EffectiveValue`1.cs index 3e20dcce56..0d93e9d8ed 100644 --- a/src/Avalonia.Base/PropertyStore/EffectiveValue`1.cs +++ b/src/Avalonia.Base/PropertyStore/EffectiveValue`1.cs @@ -57,7 +57,7 @@ namespace Avalonia.PropertyStore Debug.Assert(priority != BindingPriority.LocalValue); UpdateValueEntry(value, priority); - SetAndRaiseCore(owner, (StyledProperty)value.Property, GetValue(value), priority); + SetAndRaiseCore(owner, (StyledProperty)value.Property, GetValue(value), priority, false); } public void SetLocalValueAndRaise( @@ -65,7 +65,16 @@ namespace Avalonia.PropertyStore StyledProperty property, T value) { - SetAndRaiseCore(owner, property, value, BindingPriority.LocalValue); + SetAndRaiseCore(owner, property, value, BindingPriority.LocalValue, false); + } + + public void SetCurrentValueAndRaise( + ValueStore owner, + StyledProperty property, + T value) + { + IsOverridenCurrentValue = true; + SetAndRaiseCore(owner, property, value, Priority, true); } public bool TryGetBaseValue([MaybeNullWhen(false)] out T value) @@ -98,7 +107,7 @@ namespace Avalonia.PropertyStore Debug.Assert(Priority != BindingPriority.Animation); Debug.Assert(BasePriority != BindingPriority.Unset); UpdateValueEntry(null, BindingPriority.Animation); - SetAndRaiseCore(owner, (StyledProperty)property, _baseValue!, BasePriority); + SetAndRaiseCore(owner, (StyledProperty)property, _baseValue!, BasePriority, false); } public override void CoerceValue(ValueStore owner, AvaloniaProperty property) @@ -158,15 +167,16 @@ namespace Avalonia.PropertyStore ValueStore owner, StyledProperty property, T value, - BindingPriority priority) + BindingPriority priority, + bool isOverriddenCurrentValue) { - Debug.Assert(priority < BindingPriority.Inherited); - var oldValue = Value; var valueChanged = false; var baseValueChanged = false; var v = value; + IsOverridenCurrentValue = isOverriddenCurrentValue; + if (_uncommon?._coerce is { } coerce) v = coerce(owner.Owner, value); @@ -209,7 +219,6 @@ namespace Avalonia.PropertyStore T baseValue, BindingPriority basePriority) { - Debug.Assert(priority < BindingPriority.Inherited); Debug.Assert(basePriority > BindingPriority.Animation); Debug.Assert(priority <= basePriority); @@ -225,7 +234,7 @@ namespace Avalonia.PropertyStore bv = coerce(owner.Owner, baseValue); } - if (priority != BindingPriority.Unset && !EqualityComparer.Default.Equals(Value, v)) + if (!EqualityComparer.Default.Equals(Value, v)) { Value = v; valueChanged = true; @@ -233,9 +242,7 @@ namespace Avalonia.PropertyStore _uncommon._uncoercedValue = value; } - if (priority != BindingPriority.Unset && - (BasePriority == BindingPriority.Unset || - !EqualityComparer.Default.Equals(_baseValue, bv))) + if (!EqualityComparer.Default.Equals(_baseValue, bv)) { _baseValue = v; baseValueChanged = true; diff --git a/src/Avalonia.Base/PropertyStore/ValueStore.cs b/src/Avalonia.Base/PropertyStore/ValueStore.cs index a758360545..fd5cd91a6c 100644 --- a/src/Avalonia.Base/PropertyStore/ValueStore.cs +++ b/src/Avalonia.Base/PropertyStore/ValueStore.cs @@ -7,7 +7,6 @@ using Avalonia.Data; using Avalonia.Diagnostics; using Avalonia.Styling; using Avalonia.Utilities; -using static Avalonia.Rendering.Composition.Animations.PropertySetSnapshot; namespace Avalonia.PropertyStore { @@ -159,8 +158,9 @@ namespace Avalonia.PropertyStore public void ClearLocalValue(AvaloniaProperty property) { if (TryGetEffectiveValue(property, out var effective) && - effective.Priority == BindingPriority.LocalValue) + (effective.Priority == BindingPriority.LocalValue || effective.IsOverridenCurrentValue)) { + effective.IsOverridenCurrentValue = false; ReevaluateEffectiveValue(property, effective, ignoreLocalValue: true); } } @@ -209,6 +209,20 @@ namespace Avalonia.PropertyStore } } + public void SetCurrentValue(StyledProperty property, T value) + { + if (_effectiveValues.TryGetValue(property, out var v)) + { + ((EffectiveValue)v).SetCurrentValueAndRaise(this, property, value); + } + else + { + var effectiveValue = new EffectiveValue(Owner, property); + AddEffectiveValue(property, effectiveValue); + effectiveValue.SetCurrentValueAndRaise(this, property, value); + } + } + public object? GetValue(AvaloniaProperty property) { if (_effectiveValues.TryGetValue(property, out var v)) @@ -616,11 +630,13 @@ namespace Avalonia.PropertyStore { object? value; BindingPriority priority; + bool overridden = false; if (_effectiveValues.TryGetValue(property, out var v)) { value = v.Value; priority = v.Priority; + overridden = v.IsOverridenCurrentValue; } else if (property.Inherits && TryGetInheritedValue(property, out v)) { @@ -637,7 +653,8 @@ namespace Avalonia.PropertyStore property, value, priority, - null); + null, + overridden); } private int InsertFrame(ValueFrame frame) diff --git a/tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_SetCurrentValue.cs b/tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_SetCurrentValue.cs new file mode 100644 index 0000000000..16f924acba --- /dev/null +++ b/tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_SetCurrentValue.cs @@ -0,0 +1,270 @@ +using System; +using System.Reactive.Linq; +using Avalonia.Data; +using Avalonia.Diagnostics; +using Avalonia.Reactive; +using Xunit; +using Observable = Avalonia.Reactive.Observable; + +namespace Avalonia.Base.UnitTests +{ + public class AvaloniaObjectTests_SetCurrentValue + { + [Fact] + public void SetCurrentValue_Sets_Unset_Value() + { + var target = new Class1(); + + target.SetCurrentValue(Class1.FooProperty, "newvalue"); + + Assert.Equal("newvalue", target.GetValue(Class1.FooProperty)); + Assert.Equal(BindingPriority.Unset, GetPriority(target, Class1.FooProperty)); + Assert.True(IsOverridden(target, Class1.FooProperty)); + } + + [Theory] + [InlineData(BindingPriority.LocalValue)] + [InlineData(BindingPriority.Style)] + [InlineData(BindingPriority.Animation)] + public void SetCurrentValue_Overrides_Existing_Value(BindingPriority priority) + { + var target = new Class1(); + + target.SetValue(Class1.FooProperty, "oldvalue", priority); + target.SetCurrentValue(Class1.FooProperty, "newvalue"); + + Assert.Equal("newvalue", target.GetValue(Class1.FooProperty)); + Assert.Equal(priority, GetPriority(target, Class1.FooProperty)); + Assert.True(IsOverridden(target, Class1.FooProperty)); + } + + [Fact] + public void SetCurrentValue_Overrides_Inherited_Value() + { + var parent = new Class1(); + var target = new Class1 { InheritanceParent = parent }; + + parent.SetValue(Class1.InheritedProperty, "inheritedvalue"); + target.SetCurrentValue(Class1.InheritedProperty, "newvalue"); + + Assert.Equal("newvalue", target.GetValue(Class1.InheritedProperty)); + Assert.Equal(BindingPriority.Unset, GetPriority(target, Class1.InheritedProperty)); + Assert.True(IsOverridden(target, Class1.InheritedProperty)); + } + + [Fact] + public void SetCurrentValue_Is_Inherited() + { + var parent = new Class1(); + var target = new Class1 { InheritanceParent = parent }; + + parent.SetCurrentValue(Class1.InheritedProperty, "newvalue"); + + Assert.Equal("newvalue", target.GetValue(Class1.InheritedProperty)); + Assert.Equal(BindingPriority.Inherited, GetPriority(target, Class1.InheritedProperty)); + Assert.False(IsOverridden(target, Class1.InheritedProperty)); + } + + [Fact] + public void ClearValue_Clears_CurrentValue_With_Unset_Priority() + { + var target = new Class1(); + + target.SetCurrentValue(Class1.FooProperty, "newvalue"); + target.ClearValue(Class1.FooProperty); + + Assert.Equal("foodefault", target.Foo); + Assert.False(IsOverridden(target, Class1.FooProperty)); + } + + [Fact] + public void ClearValue_Clears_CurrentValue_With_Inherited_Priority() + { + var parent = new Class1(); + var target = new Class1 { InheritanceParent = parent }; + + parent.SetValue(Class1.InheritedProperty, "inheritedvalue"); + target.SetCurrentValue(Class1.InheritedProperty, "newvalue"); + target.ClearValue(Class1.InheritedProperty); + + Assert.Equal("inheritedvalue", target.Inherited); + Assert.False(IsOverridden(target, Class1.FooProperty)); + } + + [Fact] + public void ClearValue_Clears_CurrentValue_With_LocalValue_Priority() + { + var target = new Class1(); + + target.SetValue(Class1.FooProperty, "localvalue"); + target.SetCurrentValue(Class1.FooProperty, "newvalue"); + target.ClearValue(Class1.FooProperty); + + Assert.Equal("foodefault", target.Foo); + Assert.False(IsOverridden(target, Class1.FooProperty)); + } + + [Fact] + public void ClearValue_Clears_CurrentValue_With_Style_Priority() + { + var target = new Class1(); + + target.SetValue(Class1.FooProperty, "stylevalue", BindingPriority.Style); + target.SetCurrentValue(Class1.FooProperty, "newvalue"); + target.ClearValue(Class1.FooProperty); + + Assert.Equal("stylevalue", target.Foo); + Assert.False(IsOverridden(target, Class1.FooProperty)); + } + + [Fact] + public void SetCurrentValue_Can_Be_Coerced() + { + var target = new Class1(); + + target.SetCurrentValue(Class1.CoercedProperty, 60); + Assert.Equal(60, target.GetValue(Class1.CoercedProperty)); + + target.CoerceMax = 50; + target.CoerceValue(Class1.CoercedProperty); + Assert.Equal(50, target.GetValue(Class1.CoercedProperty)); + + target.CoerceMax = 100; + target.CoerceValue(Class1.CoercedProperty); + Assert.Equal(60, target.GetValue(Class1.CoercedProperty)); + } + + [Theory] + [InlineData(BindingPriority.LocalValue)] + [InlineData(BindingPriority.Style)] + [InlineData(BindingPriority.Animation)] + public void SetValue_Overrides_CurrentValue_With_Unset_Priority(BindingPriority priority) + { + var target = new Class1(); + + target.SetCurrentValue(Class1.FooProperty, "current"); + target.SetValue(Class1.FooProperty, "setvalue", priority); + + Assert.Equal("setvalue", target.Foo); + Assert.Equal(priority, GetPriority(target, Class1.FooProperty)); + Assert.False(IsOverridden(target, Class1.FooProperty)); + } + + [Fact] + public void Animation_Value_Overrides_CurrentValue_With_LocalValue_Priority() + { + var target = new Class1(); + + target.SetValue(Class1.FooProperty, "localvalue"); + target.SetCurrentValue(Class1.FooProperty, "current"); + target.SetValue(Class1.FooProperty, "setvalue", BindingPriority.Animation); + + Assert.Equal("setvalue", target.Foo); + Assert.Equal(BindingPriority.Animation, GetPriority(target, Class1.FooProperty)); + Assert.False(IsOverridden(target, Class1.FooProperty)); + } + + [Fact] + public void StyleTrigger_Value_Overrides_CurrentValue_With_Style_Priority() + { + var target = new Class1(); + + target.SetValue(Class1.FooProperty, "style", BindingPriority.Style); + target.SetCurrentValue(Class1.FooProperty, "current"); + target.SetValue(Class1.FooProperty, "setvalue", BindingPriority.StyleTrigger); + + Assert.Equal("setvalue", target.Foo); + Assert.Equal(BindingPriority.StyleTrigger, GetPriority(target, Class1.FooProperty)); + Assert.False(IsOverridden(target, Class1.FooProperty)); + } + + [Theory] + [InlineData(BindingPriority.LocalValue)] + [InlineData(BindingPriority.Style)] + [InlineData(BindingPriority.Animation)] + public void Binding_Overrides_CurrentValue_With_Unset_Priority(BindingPriority priority) + { + var target = new Class1(); + + target.SetCurrentValue(Class1.FooProperty, "current"); + + var s = target.Bind(Class1.FooProperty, Observable.SingleValue("binding"), priority); + + Assert.Equal("binding", target.Foo); + Assert.Equal(priority, GetPriority(target, Class1.FooProperty)); + Assert.False(IsOverridden(target, Class1.FooProperty)); + + s.Dispose(); + + Assert.Equal("foodefault", target.Foo); + } + + [Fact] + public void Animation_Binding_Overrides_CurrentValue_With_LocalValue_Priority() + { + var target = new Class1(); + + target.SetValue(Class1.FooProperty, "localvalue"); + target.SetCurrentValue(Class1.FooProperty, "current"); + + var s = target.Bind(Class1.FooProperty, Observable.SingleValue("binding"), BindingPriority.Animation); + + Assert.Equal("binding", target.Foo); + Assert.Equal(BindingPriority.Animation, GetPriority(target, Class1.FooProperty)); + Assert.False(IsOverridden(target, Class1.FooProperty)); + + s.Dispose(); + + Assert.Equal("current", target.Foo); + } + + [Fact] + public void StyleTrigger_Binding_Overrides_CurrentValue_With_Style_Priority() + { + var target = new Class1(); + + target.SetValue(Class1.FooProperty, "style", BindingPriority.Style); + target.SetCurrentValue(Class1.FooProperty, "current"); + + var s = target.Bind(Class1.FooProperty, Observable.SingleValue("binding"), BindingPriority.StyleTrigger); + + Assert.Equal("binding", target.Foo); + Assert.Equal(BindingPriority.StyleTrigger, GetPriority(target, Class1.FooProperty)); + Assert.False(IsOverridden(target, Class1.FooProperty)); + + s.Dispose(); + + Assert.Equal("style", target.Foo); + } + + private BindingPriority GetPriority(AvaloniaObject target, AvaloniaProperty property) + { + return target.GetDiagnostic(property).Priority; + } + + private bool IsOverridden(AvaloniaObject target, AvaloniaProperty property) + { + return target.GetDiagnostic(property).IsOverriddenCurrentValue; + } + + private class Class1 : AvaloniaObject + { + public static readonly StyledProperty FooProperty = + AvaloniaProperty.Register(nameof(Foo), "foodefault"); + public static readonly StyledProperty InheritedProperty = + AvaloniaProperty.Register(nameof(Inherited), "inheriteddefault", inherits: true); + public static readonly StyledProperty CoercedProperty = + AvaloniaProperty.Register(nameof(Coerced), coerce: Coerce); + + public string Foo => GetValue(FooProperty); + public string Inherited => GetValue(InheritedProperty); + public double Coerced => GetValue(CoercedProperty); + public double CoerceMax { get; set; } = 100; + + private static double Coerce(AvaloniaObject sender, double value) + { + return Math.Min(value, ((Class1)sender).CoerceMax); + } + } + } +} From 8741b7e4106fd0ceef9f68ddc5eb3dfae5604e8e Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Mon, 13 Feb 2023 10:53:03 +0100 Subject: [PATCH 22/70] Fix IsSet with SetCurrentValue. And add unit tests. --- src/Avalonia.Base/PropertyStore/ValueStore.cs | 7 +------ .../AvaloniaObjectTests_SetCurrentValue.cs | 16 ++++++++++++++-- 2 files changed, 15 insertions(+), 8 deletions(-) diff --git a/src/Avalonia.Base/PropertyStore/ValueStore.cs b/src/Avalonia.Base/PropertyStore/ValueStore.cs index fd5cd91a6c..64e3c498e9 100644 --- a/src/Avalonia.Base/PropertyStore/ValueStore.cs +++ b/src/Avalonia.Base/PropertyStore/ValueStore.cs @@ -249,12 +249,7 @@ namespace Avalonia.PropertyStore return false; } - public bool IsSet(AvaloniaProperty property) - { - if (_effectiveValues.TryGetValue(property, out var v)) - return v.Priority < BindingPriority.Inherited; - return false; - } + public bool IsSet(AvaloniaProperty property) => _effectiveValues.TryGetValue(property, out _); public void CoerceValue(AvaloniaProperty property) { diff --git a/tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_SetCurrentValue.cs b/tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_SetCurrentValue.cs index 16f924acba..3edf0b105a 100644 --- a/tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_SetCurrentValue.cs +++ b/tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_SetCurrentValue.cs @@ -1,8 +1,6 @@ using System; -using System.Reactive.Linq; using Avalonia.Data; using Avalonia.Diagnostics; -using Avalonia.Reactive; using Xunit; using Observable = Avalonia.Reactive.Observable; @@ -18,6 +16,7 @@ namespace Avalonia.Base.UnitTests target.SetCurrentValue(Class1.FooProperty, "newvalue"); Assert.Equal("newvalue", target.GetValue(Class1.FooProperty)); + Assert.True(target.IsSet(Class1.FooProperty)); Assert.Equal(BindingPriority.Unset, GetPriority(target, Class1.FooProperty)); Assert.True(IsOverridden(target, Class1.FooProperty)); } @@ -34,6 +33,7 @@ namespace Avalonia.Base.UnitTests target.SetCurrentValue(Class1.FooProperty, "newvalue"); Assert.Equal("newvalue", target.GetValue(Class1.FooProperty)); + Assert.True(target.IsSet(Class1.FooProperty)); Assert.Equal(priority, GetPriority(target, Class1.FooProperty)); Assert.True(IsOverridden(target, Class1.FooProperty)); } @@ -48,6 +48,7 @@ namespace Avalonia.Base.UnitTests target.SetCurrentValue(Class1.InheritedProperty, "newvalue"); Assert.Equal("newvalue", target.GetValue(Class1.InheritedProperty)); + Assert.True(target.IsSet(Class1.InheritedProperty)); Assert.Equal(BindingPriority.Unset, GetPriority(target, Class1.InheritedProperty)); Assert.True(IsOverridden(target, Class1.InheritedProperty)); } @@ -61,6 +62,7 @@ namespace Avalonia.Base.UnitTests parent.SetCurrentValue(Class1.InheritedProperty, "newvalue"); Assert.Equal("newvalue", target.GetValue(Class1.InheritedProperty)); + Assert.False(target.IsSet(Class1.FooProperty)); Assert.Equal(BindingPriority.Inherited, GetPriority(target, Class1.InheritedProperty)); Assert.False(IsOverridden(target, Class1.InheritedProperty)); } @@ -74,6 +76,7 @@ namespace Avalonia.Base.UnitTests target.ClearValue(Class1.FooProperty); Assert.Equal("foodefault", target.Foo); + Assert.False(target.IsSet(Class1.FooProperty)); Assert.False(IsOverridden(target, Class1.FooProperty)); } @@ -88,6 +91,7 @@ namespace Avalonia.Base.UnitTests target.ClearValue(Class1.InheritedProperty); Assert.Equal("inheritedvalue", target.Inherited); + Assert.False(target.IsSet(Class1.FooProperty)); Assert.False(IsOverridden(target, Class1.FooProperty)); } @@ -101,6 +105,7 @@ namespace Avalonia.Base.UnitTests target.ClearValue(Class1.FooProperty); Assert.Equal("foodefault", target.Foo); + Assert.False(target.IsSet(Class1.FooProperty)); Assert.False(IsOverridden(target, Class1.FooProperty)); } @@ -114,6 +119,7 @@ namespace Avalonia.Base.UnitTests target.ClearValue(Class1.FooProperty); Assert.Equal("stylevalue", target.Foo); + Assert.True(target.IsSet(Class1.FooProperty)); Assert.False(IsOverridden(target, Class1.FooProperty)); } @@ -146,6 +152,7 @@ namespace Avalonia.Base.UnitTests target.SetValue(Class1.FooProperty, "setvalue", priority); Assert.Equal("setvalue", target.Foo); + Assert.True(target.IsSet(Class1.FooProperty)); Assert.Equal(priority, GetPriority(target, Class1.FooProperty)); Assert.False(IsOverridden(target, Class1.FooProperty)); } @@ -160,6 +167,7 @@ namespace Avalonia.Base.UnitTests target.SetValue(Class1.FooProperty, "setvalue", BindingPriority.Animation); Assert.Equal("setvalue", target.Foo); + Assert.True(target.IsSet(Class1.FooProperty)); Assert.Equal(BindingPriority.Animation, GetPriority(target, Class1.FooProperty)); Assert.False(IsOverridden(target, Class1.FooProperty)); } @@ -174,6 +182,7 @@ namespace Avalonia.Base.UnitTests target.SetValue(Class1.FooProperty, "setvalue", BindingPriority.StyleTrigger); Assert.Equal("setvalue", target.Foo); + Assert.True(target.IsSet(Class1.FooProperty)); Assert.Equal(BindingPriority.StyleTrigger, GetPriority(target, Class1.FooProperty)); Assert.False(IsOverridden(target, Class1.FooProperty)); } @@ -191,6 +200,7 @@ namespace Avalonia.Base.UnitTests var s = target.Bind(Class1.FooProperty, Observable.SingleValue("binding"), priority); Assert.Equal("binding", target.Foo); + Assert.True(target.IsSet(Class1.FooProperty)); Assert.Equal(priority, GetPriority(target, Class1.FooProperty)); Assert.False(IsOverridden(target, Class1.FooProperty)); @@ -210,6 +220,7 @@ namespace Avalonia.Base.UnitTests var s = target.Bind(Class1.FooProperty, Observable.SingleValue("binding"), BindingPriority.Animation); Assert.Equal("binding", target.Foo); + Assert.True(target.IsSet(Class1.FooProperty)); Assert.Equal(BindingPriority.Animation, GetPriority(target, Class1.FooProperty)); Assert.False(IsOverridden(target, Class1.FooProperty)); @@ -229,6 +240,7 @@ namespace Avalonia.Base.UnitTests var s = target.Bind(Class1.FooProperty, Observable.SingleValue("binding"), BindingPriority.StyleTrigger); Assert.Equal("binding", target.Foo); + Assert.True(target.IsSet(Class1.FooProperty)); Assert.Equal(BindingPriority.StyleTrigger, GetPriority(target, Class1.FooProperty)); Assert.False(IsOverridden(target, Class1.FooProperty)); From 0ce9180d7c56b17c7b6a03c44d82161f6d031036 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Mon, 13 Feb 2023 11:41:59 +0100 Subject: [PATCH 23/70] Added untyped SetCurrentValue. --- src/Avalonia.Base/AvaloniaObject.cs | 34 +++++++++++++++++++ src/Avalonia.Base/AvaloniaProperty.cs | 7 ++++ src/Avalonia.Base/DirectPropertyBase.cs | 5 +++ src/Avalonia.Base/StyledProperty.cs | 21 ++++++++++++ .../AvaloniaObjectTests_SetCurrentValue.cs | 26 ++++++++++++++ .../AvaloniaPropertyTests.cs | 5 +++ 6 files changed, 98 insertions(+) diff --git a/src/Avalonia.Base/AvaloniaObject.cs b/src/Avalonia.Base/AvaloniaObject.cs index 93bbee12b8..5a5827d0aa 100644 --- a/src/Avalonia.Base/AvaloniaObject.cs +++ b/src/Avalonia.Base/AvaloniaObject.cs @@ -355,6 +355,40 @@ namespace Avalonia SetDirectValueUnchecked(property, value); } + /// + /// Sets the value of a dependency property without changing its value source. + /// + /// The property. + /// The value. + /// + /// This method is used by a component that programmatically sets the value of one of its + /// own properties without disabling an application's declared use of the property. The + /// method changes the effective value of the property, but existing data bindings and + /// styles will continue to work. + /// + /// The new value will have the property's current , even if + /// that priority is or + /// . + /// + public void SetCurrentValue(AvaloniaProperty property, object? value) => + property.RouteSetCurrentValue(this, value); + + /// + /// Sets the value of a dependency property without changing its value source. + /// + /// The type of the property. + /// The property. + /// The value. + /// + /// This method is used by a component that programmatically sets the value of one of its + /// own properties without disabling an application's declared use of the property. The + /// method changes the effective value of the property, but existing data bindings and + /// styles will continue to work. + /// + /// The new value will have the property's current , even if + /// that priority is or + /// . + /// public void SetCurrentValue(StyledProperty property, T value) { _ = property ?? throw new ArgumentNullException(nameof(property)); diff --git a/src/Avalonia.Base/AvaloniaProperty.cs b/src/Avalonia.Base/AvaloniaProperty.cs index 5db4d81f03..1c1d09c3f5 100644 --- a/src/Avalonia.Base/AvaloniaProperty.cs +++ b/src/Avalonia.Base/AvaloniaProperty.cs @@ -496,6 +496,13 @@ namespace Avalonia object? value, BindingPriority priority); + /// + /// Routes an untyped SetCurrentValue call to a typed call. + /// + /// The object instance. + /// The value. + internal abstract void RouteSetCurrentValue(AvaloniaObject o, object? value); + /// /// Routes an untyped Bind call to a typed call. /// diff --git a/src/Avalonia.Base/DirectPropertyBase.cs b/src/Avalonia.Base/DirectPropertyBase.cs index 9ee1eee0fa..94dfaaab01 100644 --- a/src/Avalonia.Base/DirectPropertyBase.cs +++ b/src/Avalonia.Base/DirectPropertyBase.cs @@ -152,6 +152,11 @@ namespace Avalonia return null; } + internal override void RouteSetCurrentValue(AvaloniaObject o, object? value) + { + RouteSetValue(o, value, BindingPriority.LocalValue); + } + /// /// Routes an untyped Bind call to a typed call. /// diff --git a/src/Avalonia.Base/StyledProperty.cs b/src/Avalonia.Base/StyledProperty.cs index 79d1b9202d..8e0ecf5544 100644 --- a/src/Avalonia.Base/StyledProperty.cs +++ b/src/Avalonia.Base/StyledProperty.cs @@ -220,6 +220,27 @@ namespace Avalonia } } + [UnconditionalSuppressMessage("Trimming", "IL2026", Justification = TrimmingMessages.ImplicitTypeConvertionSupressWarningMessage)] + internal override void RouteSetCurrentValue(AvaloniaObject target, object? value) + { + if (value == BindingOperations.DoNothing) + return; + + if (value == UnsetValue) + { + target.ClearValue(this); + } + else if (TypeUtilities.TryConvertImplicit(PropertyType, value, out var converted)) + { + target.SetCurrentValue(this, (TValue)converted!); + } + else + { + var type = value?.GetType().FullName ?? "(null)"; + throw new ArgumentException($"Invalid value for Property '{Name}': '{value}' ({type})"); + } + } + internal override IDisposable RouteBind( AvaloniaObject target, IObservable source, diff --git a/tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_SetCurrentValue.cs b/tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_SetCurrentValue.cs index 3edf0b105a..8ad36a583e 100644 --- a/tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_SetCurrentValue.cs +++ b/tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_SetCurrentValue.cs @@ -21,6 +21,19 @@ namespace Avalonia.Base.UnitTests Assert.True(IsOverridden(target, Class1.FooProperty)); } + [Fact] + public void SetCurrentValue_Sets_Unset_Value_Untyped() + { + var target = new Class1(); + + target.SetCurrentValue((AvaloniaProperty)Class1.FooProperty, "newvalue"); + + Assert.Equal("newvalue", target.GetValue(Class1.FooProperty)); + Assert.True(target.IsSet(Class1.FooProperty)); + Assert.Equal(BindingPriority.Unset, GetPriority(target, Class1.FooProperty)); + Assert.True(IsOverridden(target, Class1.FooProperty)); + } + [Theory] [InlineData(BindingPriority.LocalValue)] [InlineData(BindingPriority.Style)] @@ -140,6 +153,19 @@ namespace Avalonia.Base.UnitTests Assert.Equal(60, target.GetValue(Class1.CoercedProperty)); } + [Fact] + public void SetCurrentValue_Unset_Clears_CurrentValue() + { + var target = new Class1(); + + target.SetCurrentValue(Class1.FooProperty, "newvalue"); + target.SetCurrentValue(Class1.FooProperty, AvaloniaProperty.UnsetValue); + + Assert.Equal("foodefault", target.Foo); + Assert.False(target.IsSet(Class1.FooProperty)); + Assert.False(IsOverridden(target, Class1.FooProperty)); + } + [Theory] [InlineData(BindingPriority.LocalValue)] [InlineData(BindingPriority.Style)] diff --git a/tests/Avalonia.Base.UnitTests/AvaloniaPropertyTests.cs b/tests/Avalonia.Base.UnitTests/AvaloniaPropertyTests.cs index 5733159a23..a9b8a5f21b 100644 --- a/tests/Avalonia.Base.UnitTests/AvaloniaPropertyTests.cs +++ b/tests/Avalonia.Base.UnitTests/AvaloniaPropertyTests.cs @@ -179,6 +179,11 @@ namespace Avalonia.Base.UnitTests throw new NotImplementedException(); } + internal override void RouteSetCurrentValue(AvaloniaObject o, object value) + { + throw new NotImplementedException(); + } + internal override EffectiveValue CreateEffectiveValue(AvaloniaObject o) { throw new NotImplementedException(); From 3e0179e1e033763a7bdbf13af98eaa06b836cf19 Mon Sep 17 00:00:00 2001 From: Emmanuel Hansen Date: Thu, 9 Feb 2023 20:27:29 +0000 Subject: [PATCH 24/70] add composing region to text input client, improves android composition --- .../Avalonia.Android/AndroidInputMethod.cs | 40 ++++-- src/Android/Avalonia.Android/InputEditable.cs | 93 ++++++++++++ .../Platform/SkiaPlatform/TopLevelImpl.cs | 132 +++--------------- .../Input/TextInput/ITextInputMethodClient.cs | 6 + .../Presenters/TextPresenter.cs | 33 ++++- .../TextBoxTextInputMethodClient.cs | 11 ++ 6 files changed, 192 insertions(+), 123 deletions(-) create mode 100644 src/Android/Avalonia.Android/InputEditable.cs diff --git a/src/Android/Avalonia.Android/AndroidInputMethod.cs b/src/Android/Avalonia.Android/AndroidInputMethod.cs index c885a7768c..459b20c410 100644 --- a/src/Android/Avalonia.Android/AndroidInputMethod.cs +++ b/src/Android/Avalonia.Android/AndroidInputMethod.cs @@ -5,8 +5,10 @@ using Android.Text; using Android.Views; using Android.Views.InputMethods; using Avalonia.Android.Platform.SkiaPlatform; +using Avalonia.Controls.Presenters; using Avalonia.Input; using Avalonia.Input.TextInput; +using Avalonia.Reactive; namespace Avalonia.Android { @@ -39,6 +41,7 @@ namespace Avalonia.Android private readonly InputMethodManager _imm; private ITextInputMethodClient _client; private AvaloniaInputConnection _inputConnection; + private IDisposable _textChangeObservable; public AndroidInputMethod(TView host) { @@ -70,13 +73,9 @@ namespace Avalonia.Android { if (_client != null) { + _textChangeObservable?.Dispose(); _client.SurroundingTextChanged -= SurroundingTextChanged; - } - - if(_inputConnection != null) - { - _inputConnection.ComposingText = null; - _inputConnection.ComposingRegion = default; + _client.TextViewVisualChanged -= TextViewVisualChanged; } _client = client; @@ -84,6 +83,12 @@ namespace Avalonia.Android if (IsActive) { _client.SurroundingTextChanged += SurroundingTextChanged; + _client.TextViewVisualChanged += TextViewVisualChanged; + + if(_client.TextViewVisual is TextPresenter textVisual) + { + _textChangeObservable = textVisual.GetObservable(TextPresenter.TextProperty).Subscribe(new AnonymousObserver(UpdateText)); + } _host.RequestFocus(); @@ -101,6 +106,23 @@ namespace Avalonia.Android } } + private void TextViewVisualChanged(object sender, EventArgs e) + { + var textVisual = _client.TextViewVisual as TextPresenter; + _textChangeObservable?.Dispose(); + _textChangeObservable = null; + + if(textVisual != null) + { + _textChangeObservable = textVisual.GetObservable(TextPresenter.TextProperty).Subscribe(new AnonymousObserver(UpdateText)); + } + } + + private void UpdateText(string? obj) + { + (_inputConnection?.Editable as InputEditable)?.UpdateString(obj); + } + private void SurroundingTextChanged(object sender, EventArgs e) { if (IsActive && _inputConnection != null) @@ -109,12 +131,10 @@ namespace Avalonia.Android _inputConnection.SurroundingText = surroundingText; - _imm.UpdateSelection(_host, surroundingText.AnchorOffset, surroundingText.CursorOffset, surroundingText.AnchorOffset, surroundingText.CursorOffset); - - if (_inputConnection.ComposingText != null && !_inputConnection.IsCommiting && surroundingText.AnchorOffset == surroundingText.CursorOffset) + if ((_inputConnection?.Editable as InputEditable)?.IsInBatchEdit != true) { - _inputConnection.CommitText(_inputConnection.ComposingText, 0); _inputConnection.SetSelection(surroundingText.AnchorOffset, surroundingText.CursorOffset); + _imm.UpdateSelection(_host, surroundingText.AnchorOffset, surroundingText.CursorOffset, surroundingText.AnchorOffset, surroundingText.CursorOffset); } } } diff --git a/src/Android/Avalonia.Android/InputEditable.cs b/src/Android/Avalonia.Android/InputEditable.cs new file mode 100644 index 0000000000..b702b0205c --- /dev/null +++ b/src/Android/Avalonia.Android/InputEditable.cs @@ -0,0 +1,93 @@ +using System; +using Android.Runtime; +using Android.Text; +using Android.Views; +using Android.Views.InputMethods; +using Avalonia.Android.Platform.SkiaPlatform; +using Avalonia.Input; +using Avalonia.Input.Raw; +using Java.Lang; +using static System.Net.Mime.MediaTypeNames; + +namespace Avalonia.Android +{ + internal class InputEditable : SpannableStringBuilder + { + private readonly TopLevelImpl _topLevel; + private readonly IAndroidInputMethod _inputMethod; + private int _currentBatchLevel; + private string _previousText; + + public InputEditable(TopLevelImpl topLevel, IAndroidInputMethod inputMethod) + { + _topLevel = topLevel; + _inputMethod = inputMethod; + } + + public InputEditable(ICharSequence text) : base(text) + { + } + + public InputEditable(string text) : base(text) + { + } + + public InputEditable(ICharSequence text, int start, int end) : base(text, start, end) + { + } + + public InputEditable(string text, int start, int end) : base(text, start, end) + { + } + + protected InputEditable(IntPtr javaReference, JniHandleOwnership transfer) : base(javaReference, transfer) + { + } + + public bool IsInBatchEdit => _currentBatchLevel > 0; + + public void BeginBatchEdit() + { + _currentBatchLevel++; + + if(_currentBatchLevel == 1) + { + _previousText = ToString(); + } + } + + public void EndBatchEdit() + { + if (_currentBatchLevel == 1) + { + _inputMethod.Client.SelectInSurroundingText(-1, _previousText.Length); + var time = DateTime.Now.TimeOfDay; + var currentText = ToString(); + + if (string.IsNullOrEmpty(currentText)) + { + _inputMethod.View.DispatchKeyEvent(new KeyEvent(KeyEventActions.Down, Keycode.ForwardDel)); + } + else + { + var rawTextEvent = new RawTextInputEventArgs(KeyboardDevice.Instance, (ulong)time.Ticks, _topLevel.InputRoot, currentText); + _topLevel.Input(rawTextEvent); + } + _inputMethod.Client.SelectInSurroundingText(Selection.GetSelectionStart(this), Selection.GetSelectionEnd(this)); + + _previousText = ""; + } + + _currentBatchLevel--; + } + + public void UpdateString(string? text) + { + if(text != ToString()) + { + Clear(); + Insert(0, text); + } + } + } +} diff --git a/src/Android/Avalonia.Android/Platform/SkiaPlatform/TopLevelImpl.cs b/src/Android/Avalonia.Android/Platform/SkiaPlatform/TopLevelImpl.cs index 693a26f3bd..b6bcc4ac9f 100644 --- a/src/Android/Avalonia.Android/Platform/SkiaPlatform/TopLevelImpl.cs +++ b/src/Android/Avalonia.Android/Platform/SkiaPlatform/TopLevelImpl.cs @@ -410,27 +410,23 @@ namespace Avalonia.Android.Platform.SkiaPlatform { private readonly TopLevelImpl _topLevel; private readonly IAndroidInputMethod _inputMethod; + private readonly InputEditable _editable; public AvaloniaInputConnection(TopLevelImpl topLevel, IAndroidInputMethod inputMethod) : base(inputMethod.View, true) { _topLevel = topLevel; _inputMethod = inputMethod; + _editable = new InputEditable(_topLevel, _inputMethod); } public TextInputMethodSurroundingText SurroundingText { get; set; } - public string ComposingText { get; internal set; } - - public ComposingRegion? ComposingRegion { get; internal set; } - - public bool IsComposing => !string.IsNullOrEmpty(ComposingText); - public bool IsCommiting { get; private set; } + public override IEditable Editable => _editable; public override bool SetComposingRegion(int start, int end) { - //System.Diagnostics.Debug.WriteLine($"Composing Region: [{start}|{end}] {SurroundingText.Text?.Substring(start, end - start)}"); - - ComposingRegion = new ComposingRegion(start, end); + _inputMethod.Client.SetPreeditText(null); + _inputMethod.Client.SetComposingRegion(new Media.TextFormatting.TextRange(start, end)); return base.SetComposingRegion(start, end); } @@ -439,132 +435,46 @@ namespace Avalonia.Android.Platform.SkiaPlatform { var composingText = text.ToString(); - ComposingText = composingText; - - _inputMethod.Client?.SetPreeditText(ComposingText); - - return base.SetComposingText(text, newCursorPosition); - } - - public override bool FinishComposingText() - { - if (!string.IsNullOrEmpty(ComposingText)) + if (string.IsNullOrEmpty(composingText)) { - CommitText(ComposingText, ComposingText.Length); + return CommitText(text, newCursorPosition); } else { - ComposingRegion = new ComposingRegion(SurroundingText.CursorOffset, SurroundingText.CursorOffset); + return base.SetComposingText(text, newCursorPosition); } - - return base.FinishComposingText(); } - public override ICharSequence GetTextBeforeCursorFormatted(int length, [GeneratedEnum] GetTextFlags flags) + public override bool BeginBatchEdit() { - if (!string.IsNullOrEmpty(SurroundingText.Text) && length > 0) - { - var start = System.Math.Max(SurroundingText.CursorOffset - length, 0); - - var end = System.Math.Min(start + length - 1, SurroundingText.CursorOffset); - - var text = SurroundingText.Text.Substring(start, end - start); + _editable.BeginBatchEdit(); - //System.Diagnostics.Debug.WriteLine($"Text Before: {text}"); - - return new Java.Lang.String(text); - } - - return null; + return base.BeginBatchEdit(); } - public override ICharSequence GetTextAfterCursorFormatted(int length, [GeneratedEnum] GetTextFlags flags) + public override bool EndBatchEdit() { - if (!string.IsNullOrEmpty(SurroundingText.Text)) - { - var start = SurroundingText.CursorOffset; - - var end = System.Math.Min(start + length, SurroundingText.Text.Length); - - var text = SurroundingText.Text.Substring(start, end - start); - - //System.Diagnostics.Debug.WriteLine($"Text After: {text}"); + var ret = base.EndBatchEdit(); + _editable.EndBatchEdit(); - return new Java.Lang.String(text); - } + return ret; + } - return null; + public override bool FinishComposingText() + { + _inputMethod.Client?.SetComposingRegion(null); + return base.FinishComposingText(); } public override bool CommitText(ICharSequence text, int newCursorPosition) { - IsCommiting = true; - var committedText = text.ToString(); - _inputMethod.Client.SetPreeditText(null); - int? start, end; - - if(SurroundingText.CursorOffset != SurroundingText.AnchorOffset) - { - start = Math.Min(SurroundingText.CursorOffset, SurroundingText.AnchorOffset); - end = Math.Max(SurroundingText.CursorOffset, SurroundingText.AnchorOffset); - } - else if (ComposingRegion != null) - { - start = ComposingRegion?.Start; - end = ComposingRegion?.End; - - ComposingRegion = null; - } - else - { - start = end = _inputMethod.Client.SurroundingText.CursorOffset; - } - - _inputMethod.Client.SelectInSurroundingText((int)start, (int)end); - - var time = DateTime.Now.TimeOfDay; - - var rawTextEvent = new RawTextInputEventArgs(KeyboardDevice.Instance, (ulong)time.Ticks, _topLevel.InputRoot, committedText); - - _topLevel.Input(rawTextEvent); - - ComposingText = null; - - ComposingRegion = new ComposingRegion(newCursorPosition, newCursorPosition); + _inputMethod.Client?.SetComposingRegion(null); return base.CommitText(text, newCursorPosition); } - public override bool DeleteSurroundingText(int beforeLength, int afterLength) - { - var surroundingText = _inputMethod.Client.SurroundingText; - - var selectionStart = surroundingText.CursorOffset; - - _inputMethod.Client.SelectInSurroundingText(selectionStart - beforeLength, selectionStart + afterLength); - - _inputMethod.View.DispatchKeyEvent(new KeyEvent(KeyEventActions.Down, Keycode.ForwardDel)); - - surroundingText = _inputMethod.Client.SurroundingText; - - selectionStart = surroundingText.CursorOffset; - - ComposingRegion = new ComposingRegion(selectionStart, selectionStart); - - return base.DeleteSurroundingText(beforeLength, afterLength); - } - - public override bool SetSelection(int start, int end) - { - _inputMethod.Client.SelectInSurroundingText(start, end); - - ComposingRegion = new ComposingRegion(start, end); - - return base.SetSelection(start, end); - } - public override bool PerformEditorAction([GeneratedEnum] ImeAction actionCode) { switch (actionCode) diff --git a/src/Avalonia.Base/Input/TextInput/ITextInputMethodClient.cs b/src/Avalonia.Base/Input/TextInput/ITextInputMethodClient.cs index 531cf3c704..8239bc6a21 100644 --- a/src/Avalonia.Base/Input/TextInput/ITextInputMethodClient.cs +++ b/src/Avalonia.Base/Input/TextInput/ITextInputMethodClient.cs @@ -1,4 +1,5 @@ using System; +using Avalonia.Media.TextFormatting; using Avalonia.VisualTree; namespace Avalonia.Input.TextInput @@ -30,6 +31,11 @@ namespace Avalonia.Input.TextInput /// void SetPreeditText(string? text); + /// + /// Sets the current composing region. This doesn't remove the composing text from the commited text. + /// + void SetComposingRegion(TextRange? region); + /// /// Indicates if text input client is capable of providing the text around the cursor /// diff --git a/src/Avalonia.Controls/Presenters/TextPresenter.cs b/src/Avalonia.Controls/Presenters/TextPresenter.cs index 3481b1ecf3..bb6b03d59a 100644 --- a/src/Avalonia.Controls/Presenters/TextPresenter.cs +++ b/src/Avalonia.Controls/Presenters/TextPresenter.cs @@ -63,6 +63,15 @@ namespace Avalonia.Controls.Presenters o => o.PreeditText, (o, v) => o.PreeditText = v); + /// + /// Defines the property. + /// + public static readonly DirectProperty CompositionRegionProperty = + AvaloniaProperty.RegisterDirect( + nameof(CompositionRegion), + o => o.CompositionRegion, + (o, v) => o.CompositionRegion = v); + /// /// Defines the property. /// @@ -106,6 +115,7 @@ namespace Avalonia.Controls.Presenters private Rect _caretBounds; private Point _navigationPosition; private string? _preeditText; + private TextRange? _compositionRegion; static TextPresenter() { @@ -146,6 +156,12 @@ namespace Avalonia.Controls.Presenters set => SetAndRaise(PreeditTextProperty, ref _preeditText, value); } + public TextRange? CompositionRegion + { + get => _compositionRegion; + set => SetAndRaise(CompositionRegionProperty, ref _compositionRegion, value); + } + /// /// Gets or sets the font family. /// @@ -548,7 +564,20 @@ namespace Avalonia.Controls.Presenters var foreground = Foreground; - if (!string.IsNullOrEmpty(_preeditText)) + if(_compositionRegion != null) + { + var preeditHighlight = new ValueSpan(_compositionRegion?.Start ?? 0, _compositionRegion?.Length ?? 0, + new GenericTextRunProperties(typeface, FontSize, + foregroundBrush: foreground, + textDecorations: TextDecorations.Underline)); + + textStyleOverrides = new[] + { + preeditHighlight + }; + + } + else if (!string.IsNullOrEmpty(_preeditText)) { var preeditHighlight = new ValueSpan(_caretIndex, _preeditText.Length, new GenericTextRunProperties(typeface, FontSize, @@ -911,6 +940,7 @@ namespace Avalonia.Controls.Presenters break; } + case nameof(CompositionRegion): case nameof(Foreground): case nameof(FontSize): case nameof(FontStyle): @@ -931,7 +961,6 @@ namespace Avalonia.Controls.Presenters case nameof(PasswordChar): case nameof(RevealPassword): - case nameof(FlowDirection): { InvalidateTextLayout(); diff --git a/src/Avalonia.Controls/TextBoxTextInputMethodClient.cs b/src/Avalonia.Controls/TextBoxTextInputMethodClient.cs index 10c2f36f43..239501aace 100644 --- a/src/Avalonia.Controls/TextBoxTextInputMethodClient.cs +++ b/src/Avalonia.Controls/TextBoxTextInputMethodClient.cs @@ -5,6 +5,7 @@ using Avalonia.Media.TextFormatting; using Avalonia.Threading; using Avalonia.Utilities; using Avalonia.VisualTree; +using static System.Net.Mime.MediaTypeNames; namespace Avalonia.Controls { @@ -110,6 +111,16 @@ namespace Avalonia.Controls _presenter.PreeditText = text; } + public void SetComposingRegion(TextRange? region) + { + if (_presenter == null) + { + return; + } + + _presenter.CompositionRegion = region; + } + public void SelectInSurroundingText(int start, int end) { if(_parent is null ||_presenter is null) From 4edc10caba109f2dcaf669a448c056e1f99f81e2 Mon Sep 17 00:00:00 2001 From: Emmanuel Hansen Date: Sat, 11 Feb 2023 09:26:22 +0000 Subject: [PATCH 25/70] fix prediction text for browser --- src/Browser/Avalonia.Browser/AvaloniaView.cs | 31 +++++++++++++++++++ .../Avalonia.Browser/Interop/InputHelper.cs | 2 ++ .../webapp/modules/avalonia/input.ts | 20 ++++++++++++ 3 files changed, 53 insertions(+) diff --git a/src/Browser/Avalonia.Browser/AvaloniaView.cs b/src/Browser/Avalonia.Browser/AvaloniaView.cs index 294216ee03..16c2b2a0b4 100644 --- a/src/Browser/Avalonia.Browser/AvaloniaView.cs +++ b/src/Browser/Avalonia.Browser/AvaloniaView.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Diagnostics; using System.Reflection; using System.Runtime.InteropServices.JavaScript; using Avalonia.Browser.Interop; @@ -15,6 +16,7 @@ using Avalonia.Platform; using Avalonia.Rendering.Composition; using Avalonia.Threading; using SkiaSharp; +using static System.Runtime.CompilerServices.RuntimeHelpers; namespace Avalonia.Browser { @@ -94,6 +96,7 @@ namespace Avalonia.Browser InputHelper.SubscribeTextEvents( _inputElement, + OnBeforeInput, OnTextInput, OnCompositionStart, OnCompositionUpdate, @@ -316,11 +319,37 @@ namespace Avalonia.Browser return _topLevelImpl.RawTextEvent(data); } + private bool OnBeforeInput(JSObject arg, int start, int end) + { + var type = arg.GetPropertyAsString("inputType"); + Console.WriteLine(type); + if (type != "deleteByComposition") + { + if (type == "deleteContentBackward") + { + start = _inputElement.GetPropertyAsInt32("selectionStart"); + end = _inputElement.GetPropertyAsInt32("selectionEnd"); + } + else + { + start = -1; + end = -1; + } + } + + if(start != -1 && end != -1 && _client != null) + { + _client.SelectInSurroundingText(start, end); + } + return false; + } + private bool OnCompositionStart (JSObject args) { if (_client == null) return false; + Console.WriteLine("composition start"); _client.SetPreeditText(null); IsComposing = true; @@ -331,6 +360,7 @@ namespace Avalonia.Browser { if (_client == null) return false; + Console.WriteLine("composition update"); _client.SetPreeditText(args.GetPropertyAsString("data")); @@ -342,6 +372,7 @@ namespace Avalonia.Browser if (_client == null) return false; + Console.WriteLine("composition end"); IsComposing = false; _client.SetPreeditText(null); _topLevelImpl.RawTextEvent(args.GetPropertyAsString("data")!); diff --git a/src/Browser/Avalonia.Browser/Interop/InputHelper.cs b/src/Browser/Avalonia.Browser/Interop/InputHelper.cs index 7a010dc782..a816e39da8 100644 --- a/src/Browser/Avalonia.Browser/Interop/InputHelper.cs +++ b/src/Browser/Avalonia.Browser/Interop/InputHelper.cs @@ -18,6 +18,8 @@ internal static partial class InputHelper [JSImport("InputHelper.subscribeTextEvents", AvaloniaModule.MainModuleName)] public static partial void SubscribeTextEvents( JSObject htmlElement, + [JSMarshalAs>] + Func onBeforeInput, [JSMarshalAs>] Func onInput, [JSMarshalAs>] diff --git a/src/Browser/Avalonia.Browser/webapp/modules/avalonia/input.ts b/src/Browser/Avalonia.Browser/webapp/modules/avalonia/input.ts index 83e8ee7f1c..0f0e5eb512 100644 --- a/src/Browser/Avalonia.Browser/webapp/modules/avalonia/input.ts +++ b/src/Browser/Avalonia.Browser/webapp/modules/avalonia/input.ts @@ -47,6 +47,7 @@ export class InputHelper { public static subscribeTextEvents( element: HTMLInputElement, + beforeInputCallback: (args: InputEvent, start: number, end: number) => boolean, inputCallback: (type: string, data: string | null) => boolean, compositionStartCallback: (args: CompositionEvent) => boolean, compositionUpdateCallback: (args: CompositionEvent) => boolean, @@ -68,6 +69,25 @@ export class InputHelper { }; element.addEventListener("compositionstart", compositionStartHandler); + const beforeInputHandler = (args: InputEvent) => { + const ranges = args.getTargetRanges(); + let start = -1; + let end = -1; + if (ranges.length > 0) { + start = ranges[0].startOffset; + end = ranges[0].endOffset; + } + + if (args.inputType === "insertCompositionText") { + start = 2; + end = start + 2; + } + if (beforeInputCallback(args, start, end)) { + args.preventDefault(); + } + }; + element.addEventListener("beforeinput", beforeInputHandler); + const compositionUpdateHandler = (args: CompositionEvent) => { if (compositionUpdateCallback(args)) { args.preventDefault(); From ff983442470f5f851da9eebc8e83b1bf14393ad9 Mon Sep 17 00:00:00 2001 From: Emmanuel Hansen Date: Mon, 13 Feb 2023 08:39:34 +0000 Subject: [PATCH 26/70] allow multiline input on android --- samples/MobileSandbox/MainView.xaml | 4 ++-- src/Android/Avalonia.Android/AndroidInputMethod.cs | 3 ++- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/samples/MobileSandbox/MainView.xaml b/samples/MobileSandbox/MainView.xaml index 1eab13aa75..5d35ec3fec 100644 --- a/samples/MobileSandbox/MainView.xaml +++ b/samples/MobileSandbox/MainView.xaml @@ -5,8 +5,8 @@ x:DataType="mobileSandbox:MainView"> - - + +