diff --git a/native/Avalonia.Native/src/OSX/Avalonia.Native.OSX.xcodeproj/project.pbxproj b/native/Avalonia.Native/src/OSX/Avalonia.Native.OSX.xcodeproj/project.pbxproj index ace4a71a56..41d1534f8d 100644 --- a/native/Avalonia.Native/src/OSX/Avalonia.Native.OSX.xcodeproj/project.pbxproj +++ b/native/Avalonia.Native/src/OSX/Avalonia.Native.OSX.xcodeproj/project.pbxproj @@ -50,6 +50,7 @@ BC11A5BE2608D58F0017BAD0 /* automation.h in Headers */ = {isa = PBXBuildFile; fileRef = BC11A5BC2608D58F0017BAD0 /* automation.h */; }; BC11A5BF2608D58F0017BAD0 /* automation.mm in Sources */ = {isa = PBXBuildFile; fileRef = BC11A5BD2608D58F0017BAD0 /* automation.mm */; }; ED3791C42862E1F40080BD62 /* UniformTypeIdentifiers.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = ED3791C32862E1F40080BD62 /* UniformTypeIdentifiers.framework */; }; + EDF8CDCD2964CB01001EE34F /* PlatformSettings.mm in Sources */ = {isa = PBXBuildFile; fileRef = EDF8CDCC2964CB01001EE34F /* PlatformSettings.mm */; }; /* End PBXBuildFile section */ /* Begin PBXFileReference section */ @@ -103,6 +104,7 @@ BC11A5BC2608D58F0017BAD0 /* automation.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = automation.h; sourceTree = ""; }; BC11A5BD2608D58F0017BAD0 /* automation.mm */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.objcpp; path = automation.mm; sourceTree = ""; }; ED3791C32862E1F40080BD62 /* UniformTypeIdentifiers.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = UniformTypeIdentifiers.framework; path = System/Library/Frameworks/UniformTypeIdentifiers.framework; sourceTree = SDKROOT; }; + EDF8CDCC2964CB01001EE34F /* PlatformSettings.mm */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.objcpp; path = PlatformSettings.mm; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -163,6 +165,7 @@ 1A3E5EA723E9E83B00EDE661 /* rendertarget.mm */, 37A517B22159597E00FBA241 /* Screens.mm */, 37C09D8721580FE4006A6758 /* SystemDialogs.mm */, + EDF8CDCC2964CB01001EE34F /* PlatformSettings.mm */, AB7A61F02147C815003C5833 /* Products */, AB661C1C2148230E00291242 /* Frameworks */, 18391676ECF0E983F4964357 /* WindowBaseImpl.mm */, @@ -299,6 +302,7 @@ 1839151F32D1BB1AB51A7BB6 /* AvnPanelWindow.mm in Sources */, 18391AC16726CBC45856233B /* AvnWindow.mm in Sources */, 18391D8CD1756DC858DC1A09 /* PopupImpl.mm in Sources */, + EDF8CDCD2964CB01001EE34F /* PlatformSettings.mm in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; diff --git a/native/Avalonia.Native/src/OSX/AvnWindow.mm b/native/Avalonia.Native/src/OSX/AvnWindow.mm index 6611dcc73f..d3b7b4ede6 100644 --- a/native/Avalonia.Native/src/OSX/AvnWindow.mm +++ b/native/Avalonia.Native/src/OSX/AvnWindow.mm @@ -44,7 +44,7 @@ -(bool) isDialog { - return _parent->IsDialog(); + return _parent->IsModal(); } -(double) getExtendedTitleBarHeight @@ -281,6 +281,9 @@ - (void)windowDidBecomeKey:(NSNotification *_Nonnull)notification { + if (_parent == nullptr) + return; + _parent->BringToFront(); dispatch_async(dispatch_get_main_queue(), ^{ diff --git a/native/Avalonia.Native/src/OSX/PlatformSettings.mm b/native/Avalonia.Native/src/OSX/PlatformSettings.mm new file mode 100644 index 0000000000..34ceb93186 --- /dev/null +++ b/native/Avalonia.Native/src/OSX/PlatformSettings.mm @@ -0,0 +1,113 @@ +#include "common.h" + +@interface CocoaThemeObserver : NSObject +-(id)initWithCallback:(IAvnActionCallback *)callback; +@end + +class PlatformSettings : public ComSingleObject +{ + CocoaThemeObserver* observer; + +public: + FORWARD_IUNKNOWN() + virtual AvnPlatformThemeVariant GetPlatformTheme() override + { + @autoreleasepool + { + if (@available(macOS 10.14, *)) + { + if (NSApplication.sharedApplication.effectiveAppearance.name == NSAppearanceNameAqua + || NSApplication.sharedApplication.effectiveAppearance.name == NSAppearanceNameVibrantLight) { + return AvnPlatformThemeVariant::Light; + } else if (NSApplication.sharedApplication.effectiveAppearance.name == NSAppearanceNameDarkAqua + || NSApplication.sharedApplication.effectiveAppearance.name == NSAppearanceNameVibrantDark) { + return AvnPlatformThemeVariant::Dark; + } else if (NSApplication.sharedApplication.effectiveAppearance.name == NSAppearanceNameAccessibilityHighContrastAqua + || NSApplication.sharedApplication.effectiveAppearance.name == NSAppearanceNameAccessibilityHighContrastVibrantLight) { + return AvnPlatformThemeVariant::HighContrastLight; + } else if (NSApplication.sharedApplication.effectiveAppearance.name == NSAppearanceNameAccessibilityHighContrastDarkAqua + || NSApplication.sharedApplication.effectiveAppearance.name == NSAppearanceNameAccessibilityHighContrastVibrantDark) { + return AvnPlatformThemeVariant::HighContrastDark; + } + } + return AvnPlatformThemeVariant::Light; + } + } + + virtual unsigned int GetAccentColor() override + { + @autoreleasepool + { + if (@available(macOS 10.14, *)) + { + auto color = [NSColor controlAccentColor]; + return to_argb(color); + } + else + { + return 0; + } + } + } + + virtual void RegisterColorsChange(IAvnActionCallback *callback) override + { + if (@available(macOS 10.14, *)) + { + observer = [[CocoaThemeObserver alloc] initWithCallback: callback]; + [[NSApplication sharedApplication] addObserver:observer forKeyPath:@"effectiveAppearance" options:NSKeyValueObservingOptionNew context:nil]; + } + } + +private: + unsigned int to_argb(NSColor* color) + { + const CGFloat* components = CGColorGetComponents(color.CGColor); + unsigned int alpha = static_cast(CGColorGetAlpha(color.CGColor) * 0xFF); + unsigned int red = static_cast(components[0] * 0xFF); + unsigned int green = static_cast(components[1] * 0xFF); + unsigned int blue = static_cast(components[2] * 0xFF); + return (alpha << 24) + (red << 16) + (green << 8) + blue; + } +}; + +@implementation CocoaThemeObserver +{ + ComPtr _callback; +} +- (id) initWithCallback:(IAvnActionCallback *)callback{ + self = [super init]; + if (self) { + _callback = callback; + } + return self; +} + +/*- (void)didChangeValueForKey:(NSString *)key { + if([key isEqualToString:@"effectiveAppearance"]) { + _callback->Run(); + } + else { + [super didChangeValueForKey:key]; + } +}*/ + +- (void)observeValueForKeyPath:(NSString *)keyPath + ofObject:(id)object + change:(NSDictionary *)change + context:(void *)context { + if([keyPath isEqualToString:@"effectiveAppearance"]) { + _callback->Run(); + } else { + [super observeValueForKeyPath:keyPath + ofObject:object + change:change + context:context]; + } +} +@end + +extern IAvnPlatformSettings* CreatePlatformSettings() +{ + return new PlatformSettings(); +} diff --git a/native/Avalonia.Native/src/OSX/WindowBaseImpl.h b/native/Avalonia.Native/src/OSX/WindowBaseImpl.h index 7fb002e54f..4c2758f6c6 100644 --- a/native/Avalonia.Native/src/OSX/WindowBaseImpl.h +++ b/native/Avalonia.Native/src/OSX/WindowBaseImpl.h @@ -92,11 +92,13 @@ BEGIN_INTERFACE_MAP() virtual HRESULT SetTransparencyMode(AvnWindowTransparencyMode mode) override; + virtual HRESULT SetFrameThemeVariant(AvnPlatformThemeVariant variant) override; + virtual HRESULT BeginDragAndDropOperation(AvnDragDropEffects effects, AvnPoint point, IAvnClipboard *clipboard, IAvnDndResultCallback *cb, void *sourceHandle) override; - virtual bool IsDialog(); + virtual bool IsModal(); id GetWindowProtocol (); diff --git a/native/Avalonia.Native/src/OSX/WindowBaseImpl.mm b/native/Avalonia.Native/src/OSX/WindowBaseImpl.mm index 9946ad9b10..038e9a048c 100644 --- a/native/Avalonia.Native/src/OSX/WindowBaseImpl.mm +++ b/native/Avalonia.Native/src/OSX/WindowBaseImpl.mm @@ -498,6 +498,24 @@ HRESULT WindowBaseImpl::SetTransparencyMode(AvnWindowTransparencyMode mode) { return S_OK; } +HRESULT WindowBaseImpl::SetFrameThemeVariant(AvnPlatformThemeVariant variant) { + START_COM_CALL; + + NSAppearanceName appearanceName; + if (@available(macOS 10.14, *)) + { + appearanceName = variant == AvnPlatformThemeVariant::Dark ? NSAppearanceNameDarkAqua : NSAppearanceNameAqua; + } + else + { + appearanceName = variant == AvnPlatformThemeVariant::Dark ? NSAppearanceNameVibrantDark : NSAppearanceNameAqua; + } + + [Window setAppearance: [NSAppearance appearanceNamed: appearanceName]]; + + return S_OK; +} + HRESULT WindowBaseImpl::BeginDragAndDropOperation(AvnDragDropEffects effects, AvnPoint point, IAvnClipboard *clipboard, IAvnDndResultCallback *cb, void *sourceHandle) { START_COM_CALL; @@ -542,7 +560,7 @@ HRESULT WindowBaseImpl::BeginDragAndDropOperation(AvnDragDropEffects effects, Av return S_OK; } -bool WindowBaseImpl::IsDialog() { +bool WindowBaseImpl::IsModal() { return false; } diff --git a/native/Avalonia.Native/src/OSX/WindowImpl.h b/native/Avalonia.Native/src/OSX/WindowImpl.h index b4b1d4e70b..3861aaf170 100644 --- a/native/Avalonia.Native/src/OSX/WindowImpl.h +++ b/native/Avalonia.Native/src/OSX/WindowImpl.h @@ -23,7 +23,7 @@ private: NSRect _preZoomSize; bool _transitioningWindowState; bool _isClientAreaExtended; - bool _isDialog; + bool _isModal; WindowImpl* _parent; std::list _children; AvnExtendClientAreaChromeHints _extendClientHints; @@ -91,7 +91,9 @@ BEGIN_INTERFACE_MAP() virtual HRESULT SetWindowState (AvnWindowState state) override; - virtual bool IsDialog() override; + virtual bool IsModal() override; + + bool IsOwned(); virtual void BringToFront () override; diff --git a/native/Avalonia.Native/src/OSX/WindowImpl.mm b/native/Avalonia.Native/src/OSX/WindowImpl.mm index b6dacb6ce4..f345043f61 100644 --- a/native/Avalonia.Native/src/OSX/WindowImpl.mm +++ b/native/Avalonia.Native/src/OSX/WindowImpl.mm @@ -63,7 +63,7 @@ HRESULT WindowImpl::Show(bool activate, bool isDialog) { START_COM_CALL; @autoreleasepool { - _isDialog = isDialog || _parent != nullptr; + _isModal = isDialog; WindowBaseImpl::Show(activate, isDialog); @@ -97,7 +97,7 @@ HRESULT WindowImpl::SetParent(IAvnWindow *parent) { _parent = cparent; - _isDialog = _parent != nullptr; + _isModal = _parent != nullptr; if(_parent != nullptr && Window != nullptr){ // If one tries to show a child window with a minimized parent window, then the parent window will be @@ -123,7 +123,7 @@ void WindowImpl::BringToFront() { if ([Window isVisible] && ![Window isMiniaturized]) { - if(IsDialog()) + if(IsModal()) { Activate(); } @@ -150,7 +150,7 @@ bool WindowImpl::CanBecomeKeyWindow() { for(auto iterator = _children.begin(); iterator != _children.end(); iterator++) { - if((*iterator)->IsDialog()) + if((*iterator)->IsModal()) { return false; } @@ -569,8 +569,12 @@ HRESULT WindowImpl::SetWindowState(AvnWindowState state) { } } -bool WindowImpl::IsDialog() { - return _isDialog; +bool WindowImpl::IsModal() { + return _isModal; +} + +bool WindowImpl::IsOwned() { + return _parent != nullptr; } NSWindowStyleMask WindowImpl::GetStyle() { @@ -599,7 +603,7 @@ NSWindowStyleMask WindowImpl::GetStyle() { break; } - if (!IsDialog()) { + if (!IsOwned()) { s |= NSWindowStyleMaskMiniaturizable; } diff --git a/native/Avalonia.Native/src/OSX/common.h b/native/Avalonia.Native/src/OSX/common.h index 7ee7205776..972927b99d 100644 --- a/native/Avalonia.Native/src/OSX/common.h +++ b/native/Avalonia.Native/src/OSX/common.h @@ -27,6 +27,7 @@ extern IAvnMenuItem* CreateAppMenuItem(); extern IAvnMenuItem* CreateAppMenuItemSeparator(); extern IAvnApplicationCommands* CreateApplicationCommands(); extern IAvnNativeControlHost* CreateNativeControlHost(NSView* parent); +extern IAvnPlatformSettings* CreatePlatformSettings(); extern void SetAppMenu(IAvnMenu *menu); extern void SetServicesMenu (IAvnMenu* menu); extern IAvnMenu* GetAppMenu (); diff --git a/native/Avalonia.Native/src/OSX/main.mm b/native/Avalonia.Native/src/OSX/main.mm index c29d4108d0..99063e600e 100644 --- a/native/Avalonia.Native/src/OSX/main.mm +++ b/native/Avalonia.Native/src/OSX/main.mm @@ -398,6 +398,16 @@ public: } } + virtual HRESULT CreatePlatformSettings (IAvnPlatformSettings** ppv) override + { + START_COM_CALL; + + @autoreleasepool + { + *ppv = ::CreatePlatformSettings(); + return S_OK; + } + } }; extern "C" IAvaloniaNativeFactory* CreateAvaloniaNative() diff --git a/packages/Avalonia/AvaloniaBuildTasks.targets b/packages/Avalonia/AvaloniaBuildTasks.targets index ca2d4b66ed..33f22f4d02 100644 --- a/packages/Avalonia/AvaloniaBuildTasks.targets +++ b/packages/Avalonia/AvaloniaBuildTasks.targets @@ -48,8 +48,12 @@ + + <_AvaloniaResourcesInputsCacheFilePath>$(IntermediateOutputPath)/Avalonia/Resources.Inputs.cache + + - + @@ -57,7 +61,11 @@ - + + + + + - + + + + @@ -103,6 +114,9 @@ File="$(AvaloniaXamlReferencesTemporaryFilePath)" Lines="@(ReferencePathWithRefAssemblies)" Overwrite="true" /> + + + + DefaultCompileBindings="$(AvaloniaUseCompiledBindingsByDefault)"> + + diff --git a/samples/ControlCatalog/MainView.xaml.cs b/samples/ControlCatalog/MainView.xaml.cs index f3c1a68e72..1726028a3f 100644 --- a/samples/ControlCatalog/MainView.xaml.cs +++ b/samples/ControlCatalog/MainView.xaml.cs @@ -3,9 +3,11 @@ using System.Collections; using Avalonia; using Avalonia.Controls; using Avalonia.Controls.ApplicationLifetimes; +using Avalonia.LogicalTree; using Avalonia.Markup.Xaml; using Avalonia.Media; using Avalonia.Media.Immutable; +using Avalonia.Platform; using Avalonia.VisualTree; using ControlCatalog.Models; using ControlCatalog.Pages; @@ -14,10 +16,14 @@ namespace ControlCatalog { public class MainView : UserControl { + private readonly IPlatformSettings _platformSettings; + public MainView() { AvaloniaXamlLoader.Load(this); - + _platformSettings = AvaloniaLocator.Current.GetRequiredService(); + PlatformSettingsOnColorValuesChanged(_platformSettings, _platformSettings.GetColorValues()); + var sideBar = this.Get("Sidebar"); if (Application.Current?.ApplicationLifetime is IClassicDesktopStyleApplicationLifetime) @@ -37,6 +43,15 @@ namespace ControlCatalog if (themes.SelectedItem is CatalogTheme theme) { App.SetThemeVariant(theme); + + ((TopLevel?)this.GetVisualRoot())?.PlatformImpl?.SetFrameThemeVariant(theme switch + { + CatalogTheme.FluentLight => PlatformThemeVariant.Light, + CatalogTheme.FluentDark => PlatformThemeVariant.Dark, + CatalogTheme.SimpleLight => PlatformThemeVariant.Light, + CatalogTheme.SimpleDark => PlatformThemeVariant.Dark, + _ => throw new ArgumentOutOfRangeException() + }); } }; @@ -89,6 +104,62 @@ namespace ControlCatalog var decorations = this.Get("Decorations"); if (VisualRoot is Window window) decorations.SelectedIndex = (int)window.SystemDecorations; + + _platformSettings.ColorValuesChanged += PlatformSettingsOnColorValuesChanged; + PlatformSettingsOnColorValuesChanged(_platformSettings, _platformSettings.GetColorValues()); + } + + protected override void OnDetachedFromLogicalTree(LogicalTreeAttachmentEventArgs e) + { + base.OnDetachedFromLogicalTree(e); + + _platformSettings.ColorValuesChanged -= PlatformSettingsOnColorValuesChanged; + } + + private void PlatformSettingsOnColorValuesChanged(object? sender, PlatformColorValues e) + { + var themes = this.Get("Themes"); + var currentTheme = (CatalogTheme?)themes.SelectedItem ?? CatalogTheme.FluentLight; + var newTheme = (currentTheme, e.ThemeVariant) switch + { + (CatalogTheme.FluentDark, PlatformThemeVariant.Light) => CatalogTheme.FluentLight, + (CatalogTheme.FluentLight, PlatformThemeVariant.Dark) => CatalogTheme.FluentDark, + (CatalogTheme.SimpleDark, PlatformThemeVariant.Light) => CatalogTheme.SimpleLight, + (CatalogTheme.SimpleLight, PlatformThemeVariant.Dark) => CatalogTheme.SimpleDark, + _ => currentTheme + }; + themes.SelectedItem = newTheme; + + Application.Current!.Resources["SystemAccentColor"] = e.AccentColor1; + Application.Current.Resources["SystemAccentColorDark1"] = ChangeColorLuminosity(e.AccentColor1, -0.3); + Application.Current.Resources["SystemAccentColorDark2"] = ChangeColorLuminosity(e.AccentColor1, -0.5); + Application.Current.Resources["SystemAccentColorDark3"] = ChangeColorLuminosity(e.AccentColor1, -0.7); + Application.Current.Resources["SystemAccentColorLight1"] = ChangeColorLuminosity(e.AccentColor1, -0.3); + Application.Current.Resources["SystemAccentColorLight2"] = ChangeColorLuminosity(e.AccentColor1, -0.5); + Application.Current.Resources["SystemAccentColorLight3"] = ChangeColorLuminosity(e.AccentColor1, -0.7); + + static Color ChangeColorLuminosity(Color color, double luminosityFactor) + { + var red = (double)color.R; + var green = (double)color.G; + var blue = (double)color.B; + + if (luminosityFactor < 0) + { + luminosityFactor = 1 + luminosityFactor; + red *= luminosityFactor; + green *= luminosityFactor; + blue *= luminosityFactor; + } + else if (luminosityFactor >= 0) + { + red = (255 - red) * luminosityFactor + red; + green = (255 - green) * luminosityFactor + green; + blue = (255 - blue) * luminosityFactor + blue; + } + + return new Color(color.A, (byte)red, (byte)green, (byte)blue); + } } } } diff --git a/samples/ControlCatalog/Models/StateData.cs b/samples/ControlCatalog/Models/StateData.cs index bd6d186252..a7ea655cd2 100644 --- a/samples/ControlCatalog/Models/StateData.cs +++ b/samples/ControlCatalog/Models/StateData.cs @@ -6,10 +6,10 @@ public class StateData public string Abbreviation { get; private set; } public string Capital { get; private set; } - public StateData(string name, string abbreviatoin, string capital) + public StateData(string name, string abbreviation, string capital) { Name = name; - Abbreviation = abbreviatoin; + Abbreviation = abbreviation; Capital = capital; } @@ -17,4 +17,4 @@ public class StateData { return Name; } -} \ No newline at end of file +} diff --git a/samples/ControlCatalog/Pages/DialogsPage.xaml.cs b/samples/ControlCatalog/Pages/DialogsPage.xaml.cs index 3f1264fd32..1387fc0e7b 100644 --- a/samples/ControlCatalog/Pages/DialogsPage.xaml.cs +++ b/samples/ControlCatalog/Pages/DialogsPage.xaml.cs @@ -396,8 +396,8 @@ CanPickFolder: {storageProvider.CanPickFolder}"; return item.TryGetUri(out var uri) ? uri.ToString() : item.Name; } - Window GetWindow() => this.VisualRoot as Window ?? throw new NullReferenceException("Invalid Owner"); - TopLevel GetTopLevel() => this.VisualRoot as TopLevel ?? throw new NullReferenceException("Invalid Owner"); + Window GetWindow() => TopLevel.GetTopLevel(this) as Window ?? throw new NullReferenceException("Invalid Owner"); + TopLevel GetTopLevel() => TopLevel.GetTopLevel(this) ?? throw new NullReferenceException("Invalid Owner"); private void InitializeComponent() { diff --git a/samples/ControlCatalog/Pages/NotificationsPage.xaml.cs b/samples/ControlCatalog/Pages/NotificationsPage.xaml.cs index bfd49a2c00..6f83e5c366 100644 --- a/samples/ControlCatalog/Pages/NotificationsPage.xaml.cs +++ b/samples/ControlCatalog/Pages/NotificationsPage.xaml.cs @@ -27,7 +27,7 @@ namespace ControlCatalog.Pages { base.OnAttachedToVisualTree(e); - _viewModel.NotificationManager = new Avalonia.Controls.Notifications.WindowNotificationManager(VisualRoot as TopLevel); + _viewModel.NotificationManager = new Avalonia.Controls.Notifications.WindowNotificationManager(TopLevel.GetTopLevel(this)); } } } diff --git a/src/Android/Avalonia.Android/AndroidPlatform.cs b/src/Android/Avalonia.Android/AndroidPlatform.cs index 01b48ffd89..c73fd92423 100644 --- a/src/Android/Avalonia.Android/AndroidPlatform.cs +++ b/src/Android/Avalonia.Android/AndroidPlatform.cs @@ -43,7 +43,7 @@ namespace Avalonia.Android .Bind().ToTransient() .Bind().ToConstant(new WindowingPlatformStub()) .Bind().ToSingleton() - .Bind().ToSingleton() + .Bind().ToSingleton() .Bind().ToConstant(new AndroidThreadingInterface()) .Bind().ToSingleton() .Bind().ToConstant(new ChoreographerTimer()) diff --git a/src/Android/Avalonia.Android/AvaloniaMainActivity.cs b/src/Android/Avalonia.Android/AvaloniaMainActivity.cs index b24581fb8b..c06f5c74ec 100644 --- a/src/Android/Avalonia.Android/AvaloniaMainActivity.cs +++ b/src/Android/Avalonia.Android/AvaloniaMainActivity.cs @@ -1,18 +1,14 @@ using System; using Android.App; using Android.Content; -using Android.Content.Res; using Android.OS; using Android.Runtime; using Android.Views; using AndroidX.AppCompat.App; -using AndroidX.Lifecycle; - -using AndroidRect = Android.Graphics.Rect; namespace Avalonia.Android { - public abstract class AvaloniaMainActivity : AppCompatActivity, IActivityResultHandler + public abstract class AvaloniaMainActivity : AppCompatActivity, IActivityResultHandler, IActivityNavigationService { internal static object ViewContent; @@ -56,9 +52,18 @@ namespace Avalonia.Android } } - public override void OnConfigurationChanged(Configuration newConfig) + public event EventHandler BackRequested; + + public override void OnBackPressed() { - base.OnConfigurationChanged(newConfig); + var eventArgs = new AndroidBackRequestedEventArgs(); + + BackRequested?.Invoke(this, eventArgs); + + if (!eventArgs.Handled) + { + base.OnBackPressed(); + } } protected override void OnDestroy() diff --git a/src/Android/Avalonia.Android/AvaloniaView.cs b/src/Android/Avalonia.Android/AvaloniaView.cs index f7e32f99db..2a345a857c 100644 --- a/src/Android/Avalonia.Android/AvaloniaView.cs +++ b/src/Android/Avalonia.Android/AvaloniaView.cs @@ -1,11 +1,14 @@ using System; using Android.Content; +using Android.Content.Res; using Android.Runtime; using Android.Views; using Android.Widget; +using Avalonia.Android.Platform; using Avalonia.Android.Platform.SkiaPlatform; using Avalonia.Controls; using Avalonia.Controls.Embedding; +using Avalonia.Platform; using Avalonia.Rendering; namespace Avalonia.Android @@ -26,6 +29,7 @@ namespace Avalonia.Android _root.Prepare(); this.SetBackgroundColor(global::Android.Graphics.Color.Transparent); + OnConfigurationChanged(); } internal TopLevelImpl TopLevelImpl => _view; @@ -70,6 +74,18 @@ namespace Avalonia.Android _timerSubscription?.Dispose(); } } + + protected override void OnConfigurationChanged(Configuration newConfig) + { + base.OnConfigurationChanged(newConfig); + OnConfigurationChanged(); + } + + private void OnConfigurationChanged() + { + var settings = AvaloniaLocator.Current.GetRequiredService() as AndroidPlatformSettings; + settings?.OnViewConfigurationChanged(Context); + } class ViewImpl : TopLevelImpl { diff --git a/src/Android/Avalonia.Android/IAndroidNavigationService.cs b/src/Android/Avalonia.Android/IAndroidNavigationService.cs new file mode 100644 index 0000000000..5fa93971d9 --- /dev/null +++ b/src/Android/Avalonia.Android/IAndroidNavigationService.cs @@ -0,0 +1,14 @@ +using System; + +namespace Avalonia.Android +{ + public interface IActivityNavigationService + { + event EventHandler BackRequested; + } + + public class AndroidBackRequestedEventArgs : EventArgs + { + public bool Handled { get; set; } + } +} diff --git a/src/Android/Avalonia.Android/Platform/AndroidPlatformSettings.cs b/src/Android/Avalonia.Android/Platform/AndroidPlatformSettings.cs new file mode 100644 index 0000000000..0872603e7b --- /dev/null +++ b/src/Android/Avalonia.Android/Platform/AndroidPlatformSettings.cs @@ -0,0 +1,94 @@ +using System; +using Android; +using Android.Content; +using Android.Content.Res; +using Android.Graphics; +using Android.Provider; +using Android.Views.Accessibility; +using AndroidX.Core.Content.Resources; +using Avalonia.Media; +using Avalonia.Platform; +using Color = Avalonia.Media.Color; + +namespace Avalonia.Android.Platform; + +// TODO: ideally should be created per view/activity. +internal class AndroidPlatformSettings : DefaultPlatformSettings +{ + private PlatformColorValues _latestValues; + + public AndroidPlatformSettings() + { + _latestValues = base.GetColorValues(); + } + + public override PlatformColorValues GetColorValues() + { + return _latestValues; + } + + internal void OnViewConfigurationChanged(Context context) + { + if (context.Resources?.Configuration is null) + { + return; + } + + var systemTheme = (context.Resources.Configuration.UiMode & UiMode.NightMask) switch + { + UiMode.NightYes => PlatformThemeVariant.Dark, + UiMode.NightNo => PlatformThemeVariant.Light, + _ => throw new ArgumentOutOfRangeException() + }; + + if (OperatingSystem.IsAndroidVersionAtLeast(31)) + { + // See https://developer.android.com/reference/android/R.color + var accent1 = context.Resources.GetColor(17170494, context.Theme); // Resource.Color.SystemAccent1500 + var accent2 = context.Resources.GetColor(17170507, context.Theme); // Resource.Color.SystemAccent2500 + var accent3 = context.Resources.GetColor(17170520, context.Theme); // Resource.Color.SystemAccent3500 + + _latestValues = new PlatformColorValues + { + ThemeVariant = systemTheme, + ContrastPreference = IsHighContrast(context), + AccentColor1 = new Color(accent1.A, accent1.R, accent1.G, accent1.B), + AccentColor2 = new Color(accent2.A, accent2.R, accent2.G, accent2.B), + AccentColor3 = new Color(accent3.A, accent3.R, accent3.G, accent3.B), + }; + } + else if (OperatingSystem.IsAndroidVersionAtLeast(23)) + { + // See https://developer.android.com/reference/android/R.attr + var array = context.Theme.ObtainStyledAttributes(new[] { 16843829 }); // Resource.Attribute.ColorAccent + var accent = array.GetColor(0, 0); + + _latestValues = new PlatformColorValues + { + ThemeVariant = systemTheme, + ContrastPreference = IsHighContrast(context), + AccentColor1 = new Color(accent.A, accent.R, accent.G, accent.B) + }; + array.Recycle(); + } + else + { + _latestValues = _latestValues with { ThemeVariant = systemTheme }; + } + + OnColorValuesChanged(_latestValues); + } + + private static ColorContrastPreference IsHighContrast(Context context) + { + try + { + return Settings.Secure.GetInt(context.ContentResolver, "high_text_contrast_enabled", 0) == 1 + ? ColorContrastPreference.High : ColorContrastPreference.NoPreference; + } + catch + { + return ColorContrastPreference.NoPreference; + } + } +} diff --git a/src/Android/Avalonia.Android/Platform/AndroidSystemNavigationManager.cs b/src/Android/Avalonia.Android/Platform/AndroidSystemNavigationManager.cs new file mode 100644 index 0000000000..bb9dc66f5a --- /dev/null +++ b/src/Android/Avalonia.Android/Platform/AndroidSystemNavigationManager.cs @@ -0,0 +1,28 @@ +using System; +using Avalonia.Interactivity; +using Avalonia.Platform; + +namespace Avalonia.Android.Platform +{ + internal class AndroidSystemNavigationManager : ISystemNavigationManager + { + public event EventHandler BackRequested; + + public AndroidSystemNavigationManager(IActivityNavigationService? navigationService) + { + if(navigationService != null) + { + navigationService.BackRequested += OnBackRequested; + } + } + + private void OnBackRequested(object sender, AndroidBackRequestedEventArgs e) + { + var routedEventArgs = new RoutedEventArgs(); + + BackRequested?.Invoke(this, routedEventArgs); + + e.Handled = routedEventArgs.Handled; + } + } +} diff --git a/src/Android/Avalonia.Android/Platform/SkiaPlatform/TopLevelImpl.cs b/src/Android/Avalonia.Android/Platform/SkiaPlatform/TopLevelImpl.cs index 1aac567dda..0e452b0bdd 100644 --- a/src/Android/Avalonia.Android/Platform/SkiaPlatform/TopLevelImpl.cs +++ b/src/Android/Avalonia.Android/Platform/SkiaPlatform/TopLevelImpl.cs @@ -26,12 +26,14 @@ using Avalonia.Rendering.Composition; using Java.Lang; using Math = System.Math; using AndroidRect = Android.Graphics.Rect; +using Window = Android.Views.Window; using Android.Graphics.Drawables; namespace Avalonia.Android.Platform.SkiaPlatform { class TopLevelImpl : IAndroidView, ITopLevelImpl, EglGlPlatformSurface.IEglWindowGlPlatformSurfaceInfo, - ITopLevelImplWithTextInputMethod, ITopLevelImplWithNativeControlHost, ITopLevelImplWithStorageProvider + ITopLevelImplWithTextInputMethod, ITopLevelImplWithNativeControlHost, ITopLevelImplWithStorageProvider, + ITopLevelWithSystemNavigationManager { private readonly IGlPlatformSurface _gl; private readonly IFramebufferPlatformSurface _framebuffer; @@ -57,6 +59,8 @@ namespace Avalonia.Android.Platform.SkiaPlatform NativeControlHost = new AndroidNativeControlHostImpl(avaloniaView); StorageProvider = new AndroidStorageProvider((Activity)avaloniaView.Context); + + SystemNavigationManager = new AndroidSystemNavigationManager(avaloniaView.Context as IActivityNavigationService); } public virtual Point GetAvaloniaPointFromEvent(MotionEvent e, int pointerIndex) => @@ -286,6 +290,11 @@ namespace Avalonia.Android.Platform.SkiaPlatform public WindowTransparencyLevel TransparencyLevel { get; private set; } + public void SetFrameThemeVariant(PlatformThemeVariant themeVariant) + { + // TODO adjust status bar depending on full screen mode. + } + public AcrylicPlatformCompensationLevels AcrylicCompensationLevels => new AcrylicPlatformCompensationLevels(1, 1, 1); IntPtr EglGlPlatformSurface.IEglWindowGlPlatformSurfaceInfo.Handle => ((IPlatformHandle)_view).Handle; @@ -300,6 +309,8 @@ namespace Avalonia.Android.Platform.SkiaPlatform public IStorageProvider StorageProvider { get; } + public ISystemNavigationManager SystemNavigationManager { get; } + public void SetTransparencyLevelHint(WindowTransparencyLevel transparencyLevel) { if (TransparencyLevel != transparencyLevel) diff --git a/src/Avalonia.Base/Input/GestureRecognizers/ScrollGestureRecognizer.cs b/src/Avalonia.Base/Input/GestureRecognizers/ScrollGestureRecognizer.cs index cec98ec66b..790439245a 100644 --- a/src/Avalonia.Base/Input/GestureRecognizers/ScrollGestureRecognizer.cs +++ b/src/Avalonia.Base/Input/GestureRecognizers/ScrollGestureRecognizer.cs @@ -189,7 +189,14 @@ namespace Avalonia.Input.GestureRecognizers var speed = _inertia * Math.Pow(0.15, st.Elapsed.TotalSeconds); var distance = speed * elapsedSinceLastTick.TotalSeconds; - _target!.RaiseEvent(new ScrollGestureEventArgs(_gestureId, distance)); + var scrollGestureEventArgs = new ScrollGestureEventArgs(_gestureId, distance); + _target!.RaiseEvent(scrollGestureEventArgs); + + if (!scrollGestureEventArgs.Handled || scrollGestureEventArgs.ShouldEndScrollGesture) + { + EndGesture(); + return false; + } // EndGesture using InertialScrollSpeedEnd only in the direction of scrolling if (CanVerticallyScroll && CanHorizontallyScroll && Math.Abs(speed.X) < InertialScrollSpeedEnd && Math.Abs(speed.Y) <= InertialScrollSpeedEnd) diff --git a/src/Avalonia.Base/Input/Platform/PlatformHotkeyConfiguration.cs b/src/Avalonia.Base/Input/Platform/PlatformHotkeyConfiguration.cs index 1fc46dd396..b87ca5eded 100644 --- a/src/Avalonia.Base/Input/Platform/PlatformHotkeyConfiguration.cs +++ b/src/Avalonia.Base/Input/Platform/PlatformHotkeyConfiguration.cs @@ -81,6 +81,10 @@ namespace Avalonia.Input.Platform { new KeyGesture(Key.Apps) }; + Back = new List + { + new KeyGesture(Key.Left, KeyModifiers.Alt) + }; } public KeyModifiers CommandModifiers { get; set; } @@ -101,5 +105,6 @@ namespace Avalonia.Input.Platform public List MoveCursorToTheStartOfDocumentWithSelection { get; set; } public List MoveCursorToTheEndOfDocumentWithSelection { get; set; } public List OpenContextMenu { get; set; } + public List Back { get; set; } } } diff --git a/src/Avalonia.Base/Input/ScrollGestureEventArgs.cs b/src/Avalonia.Base/Input/ScrollGestureEventArgs.cs index fd1d0f42c3..f1a0887b60 100644 --- a/src/Avalonia.Base/Input/ScrollGestureEventArgs.cs +++ b/src/Avalonia.Base/Input/ScrollGestureEventArgs.cs @@ -6,6 +6,10 @@ namespace Avalonia.Input { public int Id { get; } public Vector Delta { get; } + /// + /// When set the ScrollGestureRecognizer should stop its current active scroll gesture. + /// + public bool ShouldEndScrollGesture { get; set; } private static int _nextId = 1; public static int GetNextFreeId() => _nextId++; diff --git a/src/Avalonia.Base/Interactivity/EventRoute.cs b/src/Avalonia.Base/Interactivity/EventRoute.cs index 3491465a68..37539a7153 100644 --- a/src/Avalonia.Base/Interactivity/EventRoute.cs +++ b/src/Avalonia.Base/Interactivity/EventRoute.cs @@ -143,10 +143,7 @@ namespace Avalonia.Interactivity // If we've got to a new control then call any RoutedEvent.Raised listeners. if (entry.Target != lastTarget) { - if (!e.Handled) - { - _event.InvokeRaised(entry.Target, e); - } + _event.InvokeRaised(entry.Target, e); // If this is a direct event and we've already raised events then we're finished. if (e.Route == RoutingStrategies.Direct && lastTarget is object) diff --git a/src/Avalonia.Base/Media/FormattedText.cs b/src/Avalonia.Base/Media/FormattedText.cs index 774580415a..0bab473442 100644 --- a/src/Avalonia.Base/Media/FormattedText.cs +++ b/src/Avalonia.Base/Media/FormattedText.cs @@ -1610,12 +1610,9 @@ namespace Avalonia.Media var thatFormatRider = new SpanRider(_that._formatRuns, _that._latestPosition, textSourceCharacterIndex); + var text = _that._text.AsMemory(textSourceCharacterIndex, thatFormatRider.Length); TextRunProperties properties = (GenericTextRunProperties)thatFormatRider.CurrentElement!; - - var textCharacters = new TextCharacters(_that._text, textSourceCharacterIndex, thatFormatRider.Length, - properties); - - return textCharacters; + return new TextCharacters(text, properties); } } } diff --git a/src/Avalonia.Base/Media/Geometry.cs b/src/Avalonia.Base/Media/Geometry.cs index 9ef32c7d09..ab1bb3b9fa 100644 --- a/src/Avalonia.Base/Media/Geometry.cs +++ b/src/Avalonia.Base/Media/Geometry.cs @@ -1,12 +1,16 @@ using System; using Avalonia.Platform; +using System.ComponentModel; +using System.Globalization; using Avalonia.Reactive; + namespace Avalonia.Media { /// /// Defines a geometric shape. - /// + /// + [TypeConverter(typeof(GeometryTypeConverter))] public abstract class Geometry : AvaloniaObject { /// @@ -201,4 +205,32 @@ namespace Avalonia.Media return new CombinedGeometry(combineMode, geometry1, geometry2, transform); } } + + public class GeometryTypeConverter : TypeConverter + { + public override bool CanConvertFrom(ITypeDescriptorContext? context, Type sourceType) + { + if (sourceType == typeof(string)) + { + return true; + } + return base.CanConvertFrom(context, sourceType); + } + + public override object? ConvertFrom(ITypeDescriptorContext? context, CultureInfo? culture, object value) + { + if (value is null) + { + throw GetConvertFromException(value); + } + string? source = value as string; + + if (source != null) + { + return Geometry.Parse(source); + } + + return base.ConvertFrom(context, culture, value); + } + } } diff --git a/src/Avalonia.Base/Media/GlyphRun.cs b/src/Avalonia.Base/Media/GlyphRun.cs index af9e458a28..fc4bc6aa1c 100644 --- a/src/Avalonia.Base/Media/GlyphRun.cs +++ b/src/Avalonia.Base/Media/GlyphRun.cs @@ -21,7 +21,7 @@ namespace Avalonia.Media private Point? _baselineOrigin; private GlyphRunMetrics? _glyphRunMetrics; - private IReadOnlyList _characters; + private ReadOnlyMemory _characters; private IReadOnlyList _glyphIndices; private IReadOnlyList? _glyphAdvances; private IReadOnlyList? _glyphOffsets; @@ -41,7 +41,7 @@ namespace Avalonia.Media public GlyphRun( IGlyphTypeface glyphTypeface, double fontRenderingEmSize, - IReadOnlyList characters, + ReadOnlyMemory characters, IReadOnlyList glyphIndices, IReadOnlyList? glyphAdvances = null, IReadOnlyList? glyphOffsets = null, @@ -141,7 +141,7 @@ namespace Avalonia.Media /// /// Gets or sets the list of UTF16 code points that represent the Unicode content of the . /// - public IReadOnlyList Characters + public ReadOnlyMemory Characters { get => _characters; set => Set(ref _characters, value); @@ -600,9 +600,9 @@ namespace Avalonia.Media } } - if (Characters != null) + if (!Characters.IsEmpty) { - clusterLength = Characters.Count - characterLength; + clusterLength = Characters.Length - characterLength; } else { @@ -653,10 +653,10 @@ namespace Avalonia.Media } else { - if (Characters != null && Characters.Count > 0) + if (!Characters.IsEmpty) { firstCluster = 0; - lastCluster = Characters.Count - 1; + lastCluster = Characters.Length - 1; } } @@ -716,14 +716,15 @@ namespace Avalonia.Media glyphCount = 0; newLineLength = 0; var trailingWhitespaceLength = 0; + var charactersSpan = _characters.Span; - if (Characters != null) + if (!charactersSpan.IsEmpty) { if (GlyphClusters == null) { - for (var i = _characters.Count - 1; i >= 0;) + for (var i = charactersSpan.Length - 1; i >= 0;) { - var codepoint = Codepoint.ReadAt(_characters, i, out var count); + var codepoint = Codepoint.ReadAt(charactersSpan, i, out var count); if (!codepoint.IsWhiteSpace) { @@ -743,55 +744,52 @@ namespace Avalonia.Media } else { - if (Characters.Count > 0) + var characterIndex = charactersSpan.Length - 1; + + for (var i = GlyphClusters.Count - 1; i >= 0; i--) { - var characterIndex = Characters.Count - 1; + var currentCluster = GlyphClusters[i]; + var codepoint = Codepoint.ReadAt(charactersSpan, characterIndex, out var characterLength); - for (var i = GlyphClusters.Count - 1; i >= 0; i--) - { - var currentCluster = GlyphClusters[i]; - var codepoint = Codepoint.ReadAt(_characters, characterIndex, out var characterLength); + characterIndex -= characterLength; - characterIndex -= characterLength; + if (!codepoint.IsWhiteSpace) + { + break; + } - if (!codepoint.IsWhiteSpace) - { - break; - } + var clusterLength = 1; - var clusterLength = 1; + while (i - 1 >= 0) + { + var nextCluster = GlyphClusters[i - 1]; - while (i - 1 >= 0) + if (currentCluster == nextCluster) { - var nextCluster = GlyphClusters[i - 1]; + clusterLength++; + i--; - if (currentCluster == nextCluster) + if(characterIndex >= 0) { - clusterLength++; - i--; - - if(characterIndex >= 0) - { - codepoint = Codepoint.ReadAt(_characters, characterIndex, out characterLength); + codepoint = Codepoint.ReadAt(charactersSpan, characterIndex, out characterLength); - characterIndex -= characterLength; - } - - continue; + characterIndex -= characterLength; } - break; - } - - if (codepoint.IsBreakChar) - { - newLineLength += clusterLength; + continue; } - trailingWhitespaceLength += clusterLength; + break; + } - glyphCount++; + if (codepoint.IsBreakChar) + { + newLineLength += clusterLength; } + + trailingWhitespaceLength += clusterLength; + + glyphCount++; } } } @@ -804,14 +802,15 @@ namespace Avalonia.Media glyphCount = 0; newLineLength = 0; var trailingWhitespaceLength = 0; + var charactersSpan = Characters.Span; - if (Characters != null) + if (!charactersSpan.IsEmpty) { if (GlyphClusters == null) { - for (var i = 0; i < Characters.Count;) + for (var i = 0; i < charactersSpan.Length;) { - var codepoint = Codepoint.ReadAt(_characters, i, out var count); + var codepoint = Codepoint.ReadAt(charactersSpan, i, out var count); if (!codepoint.IsWhiteSpace) { @@ -836,7 +835,7 @@ namespace Avalonia.Media for (var i = 0; i < GlyphClusters.Count; i++) { var currentCluster = GlyphClusters[i]; - var codepoint = Codepoint.ReadAt(_characters, characterIndex, out var characterLength); + var codepoint = Codepoint.ReadAt(charactersSpan, characterIndex, out var characterLength); characterIndex += characterLength; @@ -918,7 +917,7 @@ namespace Avalonia.Media _glyphRunImpl = platformRenderInterface.CreateGlyphRun(GlyphTypeface, FontRenderingEmSize, GlyphIndices, GlyphAdvances, GlyphOffsets); } - void IDisposable.Dispose() + public void Dispose() { _glyphRunImpl?.Dispose(); } diff --git a/src/Avalonia.Base/Media/TextDecoration.cs b/src/Avalonia.Base/Media/TextDecoration.cs index 0a7328125a..d6b2841214 100644 --- a/src/Avalonia.Base/Media/TextDecoration.cs +++ b/src/Avalonia.Base/Media/TextDecoration.cs @@ -1,5 +1,10 @@ -using Avalonia.Collections; +using System; +using System.Collections.Generic; +using Avalonia.Collections; +using Avalonia.Collections.Pooled; using Avalonia.Media.TextFormatting; +using Avalonia.Platform; +using Avalonia.Utilities; namespace Avalonia.Media { @@ -209,6 +214,51 @@ namespace Avalonia.Media var pen = new Pen(Stroke ?? defaultBrush, thickness, new DashStyle(StrokeDashArray, StrokeDashOffset), StrokeLineCap); + if (Location != TextDecorationLocation.Strikethrough) + { + var offsetY = glyphRun.BaselineOrigin.Y - origin.Y; + + var intersections = glyphRun.GlyphRunImpl.GetIntersections((float)(thickness * 0.5d - offsetY), (float)(thickness * 1.5d - offsetY)); + + if (intersections != null && intersections.Count > 0) + { + var last = baselineOrigin.X; + var finalPos = last + glyphRun.Size.Width; + var end = last; + + var points = new List(); + + //math is taken from chrome's source code. + for (var i = 0; i < intersections.Count; i += 2) + { + var start = intersections[i] - thickness; + end = intersections[i + 1] + thickness; + if (start > last && last + textMetrics.FontRenderingEmSize / 12 < start) + { + points.Add(last); + points.Add(start); + } + last = end; + } + + if (end < finalPos) + { + points.Add(end); + points.Add(finalPos); + } + + for (var i = 0; i < points.Count; i += 2) + { + var a = new Point(points[i], origin.Y); + var b = new Point(points[i + 1], origin.Y); + + drawingContext.DrawLine(pen, a, b); + } + + return; + } + } + drawingContext.DrawLine(pen, origin, origin + new Point(glyphRun.Metrics.Width, 0)); } } diff --git a/src/Avalonia.Base/Media/TextFormatting/CharacterBufferRange.cs b/src/Avalonia.Base/Media/TextFormatting/CharacterBufferRange.cs deleted file mode 100644 index 499026e8b3..0000000000 --- a/src/Avalonia.Base/Media/TextFormatting/CharacterBufferRange.cs +++ /dev/null @@ -1,275 +0,0 @@ -using System; -using System.Collections; -using System.Collections.Generic; -using System.Runtime.CompilerServices; -using Avalonia.Utilities; - -namespace Avalonia.Media.TextFormatting -{ - public readonly struct CharacterBufferRange : IReadOnlyList - { - /// - /// Getting an empty character string - /// - public static CharacterBufferRange Empty => new CharacterBufferRange(); - - /// - /// Construct from character array - /// - /// character array - /// character buffer offset to the first character - /// character length - public CharacterBufferRange( - char[] characterArray, - int offsetToFirstChar, - int characterLength - ) - : this( - new CharacterBufferReference(characterArray, offsetToFirstChar), - characterLength - ) - { } - - /// - /// Construct from string - /// - /// character string - /// character buffer offset to the first character - /// character length - public CharacterBufferRange( - string characterString, - int offsetToFirstChar, - int characterLength - ) - : this( - new CharacterBufferReference(characterString, offsetToFirstChar), - characterLength - ) - { } - - /// - /// Construct a from - /// - /// character buffer reference - /// number of characters - public CharacterBufferRange( - CharacterBufferReference characterBufferReference, - int characterLength - ) - { - if (characterLength < 0) - { - throw new ArgumentOutOfRangeException("characterLength", "ParameterCannotBeNegative"); - } - - int maxLength = characterBufferReference.CharacterBuffer.Length > 0 ? - characterBufferReference.CharacterBuffer.Length - characterBufferReference.OffsetToFirstChar : - 0; - - if (characterLength > maxLength) - { - throw new ArgumentOutOfRangeException("characterLength", $"ParameterCannotBeGreaterThan {maxLength}"); - } - - CharacterBufferReference = characterBufferReference; - Length = characterLength; - } - - /// - /// Construct a from part of another - /// - internal CharacterBufferRange( - CharacterBufferRange characterBufferRange, - int offsetToFirstChar, - int characterLength - ) : - this( - characterBufferRange.CharacterBuffer, - characterBufferRange.OffsetToFirstChar + offsetToFirstChar, - characterLength - ) - { } - - - /// - /// Construct a from string - /// - internal CharacterBufferRange( - string charString - ) : - this( - charString, - 0, - charString.Length - ) - { } - - - /// - /// Construct from memory buffer - /// - internal CharacterBufferRange( - ReadOnlyMemory charBuffer, - int offsetToFirstChar, - int characterLength - ) : - this( - new CharacterBufferReference(charBuffer, offsetToFirstChar), - characterLength - ) - { } - - - /// - /// Construct a by extracting text info from a text run - /// - internal CharacterBufferRange(TextRun textRun) - { - CharacterBufferReference = textRun.CharacterBufferReference; - Length = textRun.Length; - } - - public char this[int index] - { - [MethodImpl(MethodImplOptions.AggressiveInlining)] - get - { -#if DEBUG - if (index.CompareTo(0) < 0 || index.CompareTo(Length) > 0) - { - throw new ArgumentOutOfRangeException(nameof(index)); - } -#endif - return CharacterBuffer.Span[CharacterBufferReference.OffsetToFirstChar + index]; - } - } - - /// - /// Gets a reference to the character buffer - /// - public CharacterBufferReference CharacterBufferReference { get; } - - /// - /// Gets the number of characters in text source character store - /// - public int Length { get; } - - /// - /// Gets a span from the character buffer range - /// - public ReadOnlySpan Span => CharacterBuffer.Span.Slice(OffsetToFirstChar, Length); - - /// - /// Gets the character memory buffer - /// - internal ReadOnlyMemory CharacterBuffer => CharacterBufferReference.CharacterBuffer; - - /// - /// Gets the character offset relative to the beginning of buffer to - /// the first character of the run - /// - internal int OffsetToFirstChar => CharacterBufferReference.OffsetToFirstChar; - - /// - /// Indicate whether the character buffer range is empty - /// - internal bool IsEmpty => CharacterBuffer.Length == 0 || Length <= 0; - - internal CharacterBufferRange Take(int length) - { - if (IsEmpty) - { - return this; - } - - if (length > Length) - { - throw new ArgumentOutOfRangeException(nameof(length)); - } - - return new CharacterBufferRange(CharacterBufferReference, length); - } - - internal CharacterBufferRange Skip(int length) - { - if (IsEmpty) - { - return this; - } - - if (length > Length) - { - throw new ArgumentOutOfRangeException(nameof(length)); - } - - if (length == Length) - { - return new CharacterBufferRange(new CharacterBufferReference(), 0); - } - - var characterBufferReference = new CharacterBufferReference(CharacterBuffer, OffsetToFirstChar + length); - - return new CharacterBufferRange(characterBufferReference, Length - length); - } - - /// - /// Compute hash code - /// - public override int GetHashCode() - { - return CharacterBufferReference.GetHashCode() ^ Length; - } - - /// - /// Test equality with the input object - /// - /// The object to test - public override bool Equals(object? obj) - { - if (obj is CharacterBufferRange range) - { - return Equals(range); - } - - return false; - } - - /// - /// Test equality with the input CharacterBufferRange - /// - /// The CharacterBufferRange value to test - public bool Equals(CharacterBufferRange value) - { - return CharacterBufferReference.Equals(value.CharacterBufferReference) - && Length == value.Length; - } - - /// - /// Compare two CharacterBufferRange for equality - /// - /// left operand - /// right operand - /// whether or not two operands are equal - public static bool operator ==(CharacterBufferRange left, CharacterBufferRange right) - { - return left.Equals(right); - } - - /// - /// Compare two CharacterBufferRange for inequality - /// - /// left operand - /// right operand - /// whether or not two operands are equal - public static bool operator !=(CharacterBufferRange left, CharacterBufferRange right) - { - return !(left == right); - } - - int IReadOnlyCollection.Count => Length; - - public IEnumerator GetEnumerator() => new ImmutableReadOnlyListStructEnumerator(this); - - IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); - } -} diff --git a/src/Avalonia.Base/Media/TextFormatting/CharacterBufferReference.cs b/src/Avalonia.Base/Media/TextFormatting/CharacterBufferReference.cs deleted file mode 100644 index 672fcf3377..0000000000 --- a/src/Avalonia.Base/Media/TextFormatting/CharacterBufferReference.cs +++ /dev/null @@ -1,115 +0,0 @@ -using System; - -namespace Avalonia.Media.TextFormatting -{ - /// - /// Text character buffer reference - /// - public readonly struct CharacterBufferReference : IEquatable - { - /// - /// Construct character buffer reference from character array - /// - /// character array - /// character buffer offset to the first character - public CharacterBufferReference(char[] characterArray, int offsetToFirstChar = 0) - : this(characterArray.AsMemory(), offsetToFirstChar) - { } - - /// - /// Construct character buffer reference from string - /// - /// character string - /// character buffer offset to the first character - public CharacterBufferReference(string characterString, int offsetToFirstChar = 0) - : this(characterString.AsMemory(), offsetToFirstChar) - { } - - /// - /// Construct character buffer reference from memory buffer - /// - internal CharacterBufferReference(ReadOnlyMemory characterBuffer, int offsetToFirstChar = 0) - { - if (offsetToFirstChar < 0) - { - throw new ArgumentOutOfRangeException("offsetToFirstChar", "ParameterCannotBeNegative"); - } - - // maximum offset is one less than CharacterBuffer.Count, except that zero is always a valid offset - // even in the case of an empty or null character buffer - var maxOffset = characterBuffer.Length == 0 ? 0 : Math.Max(0, characterBuffer.Length - 1); - if (offsetToFirstChar > maxOffset) - { - throw new ArgumentOutOfRangeException("offsetToFirstChar", $"ParameterCannotBeGreaterThan, {maxOffset}"); - } - - CharacterBuffer = characterBuffer; - OffsetToFirstChar = offsetToFirstChar; - } - - /// - /// Gets the character memory buffer - /// - public ReadOnlyMemory CharacterBuffer { get; } - - /// - /// Gets the character offset relative to the beginning of buffer to - /// the first character of the run - /// - public int OffsetToFirstChar { get; } - - /// - /// Compute hash code - /// - public override int GetHashCode() - { - return CharacterBuffer.IsEmpty ? 0 : CharacterBuffer.GetHashCode(); - } - - /// - /// Test equality with the input object - /// - /// The object to test. - public override bool Equals(object? obj) - { - if (obj is CharacterBufferReference reference) - { - return Equals(reference); - } - - return false; - } - - /// - /// Test equality with the input CharacterBufferReference - /// - /// The characterBufferReference value to test - public bool Equals(CharacterBufferReference value) - { - return CharacterBuffer.Equals(value.CharacterBuffer); - } - - /// - /// Compare two CharacterBufferReference for equality - /// - /// left operand - /// right operand - /// whether or not two operands are equal - public static bool operator ==(CharacterBufferReference left, CharacterBufferReference right) - { - return left.Equals(right); - } - - /// - /// Compare two CharacterBufferReference for inequality - /// - /// left operand - /// right operand - /// whether or not two operands are equal - public static bool operator !=(CharacterBufferReference left, CharacterBufferReference right) - { - return !(left == right); - } - } -} - diff --git a/src/Avalonia.Base/Media/TextFormatting/FormattedTextSource.cs b/src/Avalonia.Base/Media/TextFormatting/FormattedTextSource.cs index 49d94b511d..5c28989c7d 100644 --- a/src/Avalonia.Base/Media/TextFormatting/FormattedTextSource.cs +++ b/src/Avalonia.Base/Media/TextFormatting/FormattedTextSource.cs @@ -7,14 +7,14 @@ namespace Avalonia.Media.TextFormatting { internal readonly struct FormattedTextSource : ITextSource { - private readonly CharacterBufferRange _text; + private readonly string _text; private readonly TextRunProperties _defaultProperties; private readonly IReadOnlyList>? _textModifier; public FormattedTextSource(string text, TextRunProperties defaultProperties, IReadOnlyList>? textModifier) { - _text = new CharacterBufferRange(text); + _text = text; _defaultProperties = defaultProperties; _textModifier = textModifier; } @@ -26,7 +26,7 @@ namespace Avalonia.Media.TextFormatting return null; } - var runText = _text.Skip(textSourceIndex); + var runText = _text.AsSpan(textSourceIndex); if (runText.IsEmpty) { @@ -35,7 +35,7 @@ namespace Avalonia.Media.TextFormatting var textStyleRun = CreateTextStyleRun(runText, textSourceIndex, _defaultProperties, _textModifier); - return new TextCharacters(runText.Take(textStyleRun.Length).CharacterBufferReference, textStyleRun.Length, textStyleRun.Value); + return new TextCharacters(_text.AsMemory(textSourceIndex, textStyleRun.Length), textStyleRun.Value); } /// @@ -48,7 +48,7 @@ namespace Avalonia.Media.TextFormatting /// /// The created text style run. /// - private static ValueSpan CreateTextStyleRun(CharacterBufferRange text, int firstTextSourceIndex, + private static ValueSpan CreateTextStyleRun(ReadOnlySpan text, int firstTextSourceIndex, TextRunProperties defaultProperties, IReadOnlyList>? textModifier) { if (textModifier == null || textModifier.Count == 0) @@ -122,7 +122,7 @@ namespace Avalonia.Media.TextFormatting return new ValueSpan(firstTextSourceIndex, length, currentProperties); } - private static int CoerceLength(CharacterBufferRange text, int length) + private static int CoerceLength(ReadOnlySpan text, int length) { var finalLength = 0; @@ -132,7 +132,7 @@ namespace Avalonia.Media.TextFormatting { var grapheme = graphemeEnumerator.Current; - finalLength += grapheme.Length; + finalLength += grapheme.Text.Length; if (finalLength >= length) { diff --git a/src/Avalonia.Base/Media/TextFormatting/InterWordJustification.cs b/src/Avalonia.Base/Media/TextFormatting/InterWordJustification.cs index 21e8ce089a..b518d47a6d 100644 --- a/src/Avalonia.Base/Media/TextFormatting/InterWordJustification.cs +++ b/src/Avalonia.Base/Media/TextFormatting/InterWordJustification.cs @@ -50,14 +50,14 @@ namespace Avalonia.Media.TextFormatting foreach (var textRun in lineImpl.TextRuns) { - var text = new CharacterBufferRange(textRun); + var text = textRun.Text; if (text.IsEmpty) { continue; } - var lineBreakEnumerator = new LineBreakEnumerator(text); + var lineBreakEnumerator = new LineBreakEnumerator(text.Span); while (lineBreakEnumerator.MoveNext()) { @@ -84,7 +84,7 @@ namespace Avalonia.Media.TextFormatting foreach (var textRun in lineImpl.TextRuns) { - var text = textRun.CharacterBufferReference.CharacterBuffer; + var text = textRun.Text; if (text.IsEmpty) { diff --git a/src/Avalonia.Base/Media/TextFormatting/ShapedBuffer.cs b/src/Avalonia.Base/Media/TextFormatting/ShapedBuffer.cs index 902b897240..b05fab08fa 100644 --- a/src/Avalonia.Base/Media/TextFormatting/ShapedBuffer.cs +++ b/src/Avalonia.Base/Media/TextFormatting/ShapedBuffer.cs @@ -1,31 +1,39 @@ using System; +using System.Buffers; using System.Collections.Generic; using Avalonia.Utilities; namespace Avalonia.Media.TextFormatting { - public sealed class ShapedBuffer : IList + public sealed class ShapedBuffer : IList, IDisposable { private static readonly IComparer s_clusterComparer = new CompareClusters(); - - public ShapedBuffer(CharacterBufferRange characterBufferRange, int bufferLength, IGlyphTypeface glyphTypeface, double fontRenderingEmSize, sbyte bidiLevel) : - this(characterBufferRange, new GlyphInfo[bufferLength], glyphTypeface, fontRenderingEmSize, bidiLevel) + private bool _bufferRented; + + public ShapedBuffer(ReadOnlyMemory text, int bufferLength, IGlyphTypeface glyphTypeface, double fontRenderingEmSize, sbyte bidiLevel) : + this(text, + new ArraySlice(ArrayPool.Shared.Rent(bufferLength), 0, bufferLength), + glyphTypeface, + fontRenderingEmSize, + bidiLevel) { - + _bufferRented = true; + Length = bufferLength; } - internal ShapedBuffer(CharacterBufferRange characterBufferRange, ArraySlice glyphInfos, IGlyphTypeface glyphTypeface, double fontRenderingEmSize, sbyte bidiLevel) + internal ShapedBuffer(ReadOnlyMemory text, ArraySlice glyphInfos, IGlyphTypeface glyphTypeface, double fontRenderingEmSize, sbyte bidiLevel) { - CharacterBufferRange = characterBufferRange; + Text = text; GlyphInfos = glyphInfos; GlyphTypeface = glyphTypeface; FontRenderingEmSize = fontRenderingEmSize; BidiLevel = bidiLevel; + Length = GlyphInfos.Length; } internal ArraySlice GlyphInfos { get; } - public int Length => GlyphInfos.Length; + public int Length { get; } public IGlyphTypeface GlyphTypeface { get; } @@ -43,7 +51,7 @@ namespace Avalonia.Media.TextFormatting public IReadOnlyList GlyphOffsets => new GlyphOffsetList(GlyphInfos); - public CharacterBufferRange CharacterBufferRange { get; } + public ReadOnlyMemory Text { get; } /// /// Finds a glyph index for given character index. @@ -105,7 +113,7 @@ namespace Avalonia.Media.TextFormatting /// The split result. internal SplitResult Split(int length) { - if (CharacterBufferRange.Length == length) + if (Text.Length == length) { return new SplitResult(this, null); } @@ -117,10 +125,10 @@ namespace Avalonia.Media.TextFormatting var glyphCount = FindGlyphIndex(start + length); - var first = new ShapedBuffer(CharacterBufferRange.Take(length), + var first = new ShapedBuffer(Text.Slice(0, length), GlyphInfos.Take(glyphCount), GlyphTypeface, FontRenderingEmSize, BidiLevel); - var second = new ShapedBuffer(CharacterBufferRange.Skip(length), + var second = new ShapedBuffer(Text.Slice(length), GlyphInfos.Skip(glyphCount), GlyphTypeface, FontRenderingEmSize, BidiLevel); return new SplitResult(first, second); @@ -260,6 +268,23 @@ namespace Avalonia.Media.TextFormatting System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator() => GetEnumerator(); } + + public void Dispose() + { + GC.SuppressFinalize(this); + if (_bufferRented) + { + GlyphInfos.ReturnRent(); + } + } + + ~ShapedBuffer() + { + if (_bufferRented) + { + GlyphInfos.ReturnRent(); + } + } } public readonly record struct GlyphInfo diff --git a/src/Avalonia.Base/Media/TextFormatting/ShapedTextRun.cs b/src/Avalonia.Base/Media/TextFormatting/ShapedTextRun.cs index 3149bc2cda..583f2e49f1 100644 --- a/src/Avalonia.Base/Media/TextFormatting/ShapedTextRun.cs +++ b/src/Avalonia.Base/Media/TextFormatting/ShapedTextRun.cs @@ -6,15 +6,13 @@ namespace Avalonia.Media.TextFormatting /// /// A text run that holds shaped characters. /// - public sealed class ShapedTextRun : DrawableTextRun + public sealed class ShapedTextRun : DrawableTextRun, IDisposable { private GlyphRun? _glyphRun; public ShapedTextRun(ShapedBuffer shapedBuffer, TextRunProperties properties) { ShapedBuffer = shapedBuffer; - CharacterBufferReference = shapedBuffer.CharacterBufferRange.CharacterBufferReference; - Length = shapedBuffer.CharacterBufferRange.Length; Properties = properties; TextMetrics = new TextMetrics(properties.Typeface.GlyphTypeface, properties.FontRenderingEmSize); } @@ -26,13 +24,15 @@ namespace Avalonia.Media.TextFormatting public ShapedBuffer ShapedBuffer { get; } /// - public override CharacterBufferReference CharacterBufferReference { get; } + public override ReadOnlyMemory Text + => ShapedBuffer.Text; /// public override TextRunProperties Properties { get; } /// - public override int Length { get; } + public override int Length + => ShapedBuffer.Text.Length; public TextMetrics TextMetrics { get; } @@ -113,6 +113,7 @@ namespace Avalonia.Media.TextFormatting { length = 0; var currentWidth = 0.0; + var charactersSpan = GlyphRun.Characters.Span; for (var i = 0; i < ShapedBuffer.Length; i++) { @@ -123,7 +124,7 @@ namespace Avalonia.Media.TextFormatting break; } - Codepoint.ReadAt(GlyphRun.Characters, length, out var count); + Codepoint.ReadAt(charactersSpan, length, out var count); length += count; currentWidth += advance; @@ -136,6 +137,7 @@ namespace Avalonia.Media.TextFormatting { length = 0; width = 0; + var charactersSpan = GlyphRun.Characters.Span; for (var i = ShapedBuffer.Length - 1; i >= 0; i--) { @@ -146,7 +148,7 @@ namespace Avalonia.Media.TextFormatting break; } - Codepoint.ReadAt(GlyphRun.Characters, length, out var count); + Codepoint.ReadAt(charactersSpan, length, out var count); length += count; width += advance; @@ -192,12 +194,18 @@ namespace Avalonia.Media.TextFormatting return new GlyphRun( ShapedBuffer.GlyphTypeface, ShapedBuffer.FontRenderingEmSize, - new CharacterBufferRange(CharacterBufferReference, Length), + Text, ShapedBuffer.GlyphIndices, ShapedBuffer.GlyphAdvances, ShapedBuffer.GlyphOffsets, ShapedBuffer.GlyphClusters, BidiLevel); } + + public void Dispose() + { + _glyphRun?.Dispose(); + ShapedBuffer.Dispose(); + } } } diff --git a/src/Avalonia.Base/Media/TextFormatting/TextCharacters.cs b/src/Avalonia.Base/Media/TextFormatting/TextCharacters.cs index 1a48151834..2525f0dbf9 100644 --- a/src/Avalonia.Base/Media/TextFormatting/TextCharacters.cs +++ b/src/Avalonia.Base/Media/TextFormatting/TextCharacters.cs @@ -10,82 +10,34 @@ namespace Avalonia.Media.TextFormatting public class TextCharacters : TextRun { /// - /// Construct a run of text content from character array + /// Constructs a run for text content from a string. /// - public TextCharacters( - char[] characterArray, - int offsetToFirstChar, - int length, - TextRunProperties textRunProperties - ) : - this( - new CharacterBufferReference(characterArray, offsetToFirstChar), - length, - textRunProperties - ) - { } - - - /// - /// Construct a run for text content from string - /// - public TextCharacters( - string characterString, - TextRunProperties textRunProperties - ) : - this( - characterString, - 0, // offsetToFirstChar - (characterString == null) ? 0 : characterString.Length, - textRunProperties - ) - { } - - /// - /// Construct a run for text content from string - /// - public TextCharacters( - string characterString, - int offsetToFirstChar, - int length, - TextRunProperties textRunProperties - ) : - this( - new CharacterBufferReference(characterString, offsetToFirstChar), - length, - textRunProperties - ) - { } + public TextCharacters(string text, TextRunProperties textRunProperties) + : this(text.AsMemory(), textRunProperties) + { + } /// - /// Internal constructor of TextContent + /// Constructs a run for text content from a memory region. /// - public TextCharacters( - CharacterBufferReference characterBufferReference, - int length, - TextRunProperties textRunProperties - ) + public TextCharacters(ReadOnlyMemory text, TextRunProperties textRunProperties) { - if (length <= 0) - { - throw new ArgumentOutOfRangeException("length", "ParameterMustBeGreaterThanZero"); - } - if (textRunProperties.FontRenderingEmSize <= 0) { - throw new ArgumentOutOfRangeException("textRunProperties.FontRenderingEmSize", "ParameterMustBeGreaterThanZero"); + throw new ArgumentOutOfRangeException(nameof(textRunProperties), textRunProperties.FontRenderingEmSize, + $"Invalid {nameof(TextRunProperties.FontRenderingEmSize)}"); } - CharacterBufferReference = characterBufferReference; - Length = length; + Text = text; Properties = textRunProperties; } /// - public override int Length { get; } + public override int Length + => Text.Length; /// - public override CharacterBufferReference CharacterBufferReference { get; } + public override ReadOnlyMemory Text { get; } /// public override TextRunProperties Properties { get; } @@ -94,17 +46,19 @@ namespace Avalonia.Media.TextFormatting /// Gets a list of . /// /// The shapeable text characters. - internal IReadOnlyList GetShapeableCharacters(CharacterBufferRange characterBufferRange, sbyte biDiLevel, ref TextRunProperties? previousProperties) + internal IReadOnlyList GetShapeableCharacters(ReadOnlyMemory text, sbyte biDiLevel, + ref TextRunProperties? previousProperties) { var shapeableCharacters = new List(2); + var properties = Properties; - while (characterBufferRange.Length > 0) + while (!text.IsEmpty) { - var shapeableRun = CreateShapeableRun(characterBufferRange, Properties, biDiLevel, ref previousProperties); + var shapeableRun = CreateShapeableRun(text, properties, biDiLevel, ref previousProperties); shapeableCharacters.Add(shapeableRun); - characterBufferRange = characterBufferRange.Skip(shapeableRun.Length); + text = text.Slice(shapeableRun.Length); previousProperties = shapeableRun.Properties; } @@ -115,45 +69,46 @@ namespace Avalonia.Media.TextFormatting /// /// Creates a shapeable text run with unique properties. /// - /// The character buffer range to create text runs from. + /// The characters to create text runs from. /// The default text run properties. /// The bidi level of the run. /// /// A list of shapeable text runs. - private static UnshapedTextRun CreateShapeableRun(CharacterBufferRange characterBufferRange, + private static UnshapedTextRun CreateShapeableRun(ReadOnlyMemory text, TextRunProperties defaultProperties, sbyte biDiLevel, ref TextRunProperties? previousProperties) { var defaultTypeface = defaultProperties.Typeface; var currentTypeface = defaultTypeface; var previousTypeface = previousProperties?.Typeface; + var textSpan = text.Span; - if (TryGetShapeableLength(characterBufferRange, currentTypeface, null, out var count, out var script)) + if (TryGetShapeableLength(textSpan, currentTypeface, null, out var count, out var script)) { if (script == Script.Common && previousTypeface is not null) { - if (TryGetShapeableLength(characterBufferRange, previousTypeface.Value, null, out var fallbackCount, out _)) + if (TryGetShapeableLength(textSpan, previousTypeface.Value, null, out var fallbackCount, out _)) { - return new UnshapedTextRun(characterBufferRange.CharacterBufferReference, fallbackCount, + return new UnshapedTextRun(text.Slice(0, fallbackCount), defaultProperties.WithTypeface(previousTypeface.Value), biDiLevel); } } - return new UnshapedTextRun(characterBufferRange.CharacterBufferReference, count, defaultProperties.WithTypeface(currentTypeface), + return new UnshapedTextRun(text.Slice(0, count), defaultProperties.WithTypeface(currentTypeface), biDiLevel); } if (previousTypeface is not null) { - if (TryGetShapeableLength(characterBufferRange, previousTypeface.Value, defaultTypeface, out count, out _)) + if (TryGetShapeableLength(textSpan, previousTypeface.Value, defaultTypeface, out count, out _)) { - return new UnshapedTextRun(characterBufferRange.CharacterBufferReference, count, + return new UnshapedTextRun(text.Slice(0, count), defaultProperties.WithTypeface(previousTypeface.Value), biDiLevel); } } var codepoint = Codepoint.ReplacementCodepoint; - var codepointEnumerator = new CodepointEnumerator(characterBufferRange.Skip(count)); + var codepointEnumerator = new CodepointEnumerator(text.Slice(count).Span); while (codepointEnumerator.MoveNext()) { @@ -173,10 +128,10 @@ namespace Avalonia.Media.TextFormatting defaultTypeface.Stretch, defaultTypeface.FontFamily, defaultProperties.CultureInfo, out currentTypeface); - if (matchFound && TryGetShapeableLength(characterBufferRange, currentTypeface, defaultTypeface, out count, out _)) + if (matchFound && TryGetShapeableLength(textSpan, currentTypeface, defaultTypeface, out count, out _)) { //Fallback found - return new UnshapedTextRun(characterBufferRange.CharacterBufferReference, count, defaultProperties.WithTypeface(currentTypeface), + return new UnshapedTextRun(text.Slice(0, count), defaultProperties.WithTypeface(currentTypeface), biDiLevel); } @@ -185,7 +140,7 @@ namespace Avalonia.Media.TextFormatting var glyphTypeface = currentTypeface.GlyphTypeface; - var enumerator = new GraphemeEnumerator(characterBufferRange); + var enumerator = new GraphemeEnumerator(textSpan); while (enumerator.MoveNext()) { @@ -196,23 +151,23 @@ namespace Avalonia.Media.TextFormatting break; } - count += grapheme.Length; + count += grapheme.Text.Length; } - return new UnshapedTextRun(characterBufferRange.CharacterBufferReference, count, defaultProperties, biDiLevel); + return new UnshapedTextRun(text.Slice(0, count), defaultProperties, biDiLevel); } /// /// Tries to get a shapeable length that is supported by the specified typeface. /// - /// The character buffer range to shape. + /// The characters to shape. /// The typeface that is used to find matching characters. /// /// The shapeable length. /// /// internal static bool TryGetShapeableLength( - CharacterBufferRange characterBufferRange, + ReadOnlySpan text, Typeface typeface, Typeface? defaultTypeface, out int length, @@ -221,7 +176,7 @@ namespace Avalonia.Media.TextFormatting length = 0; script = Script.Unknown; - if (characterBufferRange.Length == 0) + if (text.IsEmpty) { return false; } @@ -229,7 +184,7 @@ namespace Avalonia.Media.TextFormatting var font = typeface.GlyphTypeface; var defaultFont = defaultTypeface?.GlyphTypeface; - var enumerator = new GraphemeEnumerator(characterBufferRange); + var enumerator = new GraphemeEnumerator(text); while (enumerator.MoveNext()) { @@ -264,7 +219,7 @@ namespace Avalonia.Media.TextFormatting } } - length += currentGrapheme.Length; + length += currentGrapheme.Text.Length; } return length > 0; diff --git a/src/Avalonia.Base/Media/TextFormatting/TextEllipsisHelper.cs b/src/Avalonia.Base/Media/TextFormatting/TextEllipsisHelper.cs index 9c201bda22..528cd45581 100644 --- a/src/Avalonia.Base/Media/TextFormatting/TextEllipsisHelper.cs +++ b/src/Avalonia.Base/Media/TextFormatting/TextEllipsisHelper.cs @@ -45,9 +45,7 @@ namespace Avalonia.Media.TextFormatting { var currentBreakPosition = 0; - var text = new CharacterBufferRange(currentRun.CharacterBufferReference, currentRun.Length); - - var lineBreaker = new LineBreakEnumerator(text); + var lineBreaker = new LineBreakEnumerator(currentRun.Text.Span); while (currentBreakPosition < measuredLength && lineBreaker.MoveNext()) { diff --git a/src/Avalonia.Base/Media/TextFormatting/TextFormatterImpl.cs b/src/Avalonia.Base/Media/TextFormatting/TextFormatterImpl.cs index 989bf7749d..8afecb09e2 100644 --- a/src/Avalonia.Base/Media/TextFormatting/TextFormatterImpl.cs +++ b/src/Avalonia.Base/Media/TextFormatting/TextFormatterImpl.cs @@ -1,5 +1,7 @@ using System; +using System.Buffers; using System.Collections.Generic; +using System.Runtime.InteropServices; using Avalonia.Media.TextFormatting.Unicode; using Avalonia.Utilities; @@ -158,25 +160,21 @@ namespace Avalonia.Media.TextFormatting { var flowDirection = paragraphProperties.FlowDirection; var shapedRuns = new List(); - var biDiData = new BidiData((sbyte)flowDirection); + using var biDiData = new BidiData((sbyte)flowDirection); foreach (var textRun in textRuns) { - if (textRun.CharacterBufferReference.CharacterBuffer.Length == 0) + if (textRun.Text.IsEmpty) { - var characterBuffer = new CharacterBufferReference(new char[textRun.Length]); - - biDiData.Append(new CharacterBufferRange(characterBuffer, textRun.Length)); + biDiData.Append(new char[textRun.Length]); } else { - var text = new CharacterBufferRange(textRun.CharacterBufferReference, textRun.Length); - - biDiData.Append(text); + biDiData.Append(textRun.Text.Span); } } - var biDi = new BidiAlgorithm(); + using var biDi = new BidiAlgorithm(); biDi.Process(biDiData); @@ -187,10 +185,7 @@ namespace Avalonia.Media.TextFormatting var processedRuns = new List(textRuns.Count); - foreach (var coalescedRuns in CoalesceLevels(textRuns, biDi.ResolvedLevels)) - { - processedRuns.AddRange(coalescedRuns); - } + CoalesceLevels(textRuns, biDi.ResolvedLevels, processedRuns); for (var index = 0; index < processedRuns.Count; index++) { @@ -201,9 +196,7 @@ namespace Avalonia.Media.TextFormatting case UnshapedTextRun shapeableRun: { var groupedRuns = new List(2) { shapeableRun }; - var characterBufferReference = currentRun.CharacterBufferReference; - var length = currentRun.Length; - var offsetToFirstCharacter = characterBufferReference.OffsetToFirstChar; + var text = shapeableRun.Text; while (index + 1 < processedRuns.Count) { @@ -212,23 +205,14 @@ namespace Avalonia.Media.TextFormatting break; } - if (shapeableRun.CanShapeTogether(nextRun)) + if (shapeableRun.BidiLevel == nextRun.BidiLevel + && TryJoinContiguousMemories(text, nextRun.Text, out var joinedText) + && CanShapeTogether(shapeableRun.Properties, nextRun.Properties)) { groupedRuns.Add(nextRun); - - length += nextRun.Length; - - if (offsetToFirstCharacter > nextRun.CharacterBufferReference.OffsetToFirstChar) - { - offsetToFirstCharacter = nextRun.CharacterBufferReference.OffsetToFirstChar; - } - - characterBufferReference = new CharacterBufferReference(characterBufferReference.CharacterBuffer, offsetToFirstCharacter); - index++; - shapeableRun = nextRun; - + text = joinedText; continue; } @@ -240,7 +224,7 @@ namespace Avalonia.Media.TextFormatting shapeableRun.BidiLevel, currentRun.Properties.CultureInfo, paragraphProperties.DefaultIncrementalTab, paragraphProperties.LetterSpacing); - shapedRuns.AddRange(ShapeTogether(groupedRuns, characterBufferReference, length, shaperOptions)); + shapedRuns.AddRange(ShapeTogether(groupedRuns, text, shaperOptions)); break; } @@ -256,12 +240,81 @@ namespace Avalonia.Media.TextFormatting return shapedRuns; } + /// + /// Tries to join two potnetially contiguous memory regions. + /// + /// The first memory region. + /// The second memory region. + /// On success, a memory region representing the union of the two regions. + /// true if the two regions were contigous; false otherwise. + private static bool TryJoinContiguousMemories(ReadOnlyMemory x, ReadOnlyMemory y, + out ReadOnlyMemory joinedMemory) + { + if (MemoryMarshal.TryGetString(x, out var xString, out var xStart, out var xLength)) + { + if (MemoryMarshal.TryGetString(y, out var yString, out var yStart, out var yLength) + && ReferenceEquals(xString, yString) + && TryGetContiguousStart(xStart, xLength, yStart, yLength, out var joinedStart)) + { + joinedMemory = xString.AsMemory(joinedStart, xLength + yLength); + return true; + } + } + + else if (MemoryMarshal.TryGetArray(x, out var xSegment)) + { + if (MemoryMarshal.TryGetArray(y, out var ySegment) + && ReferenceEquals(xSegment.Array, ySegment.Array) + && TryGetContiguousStart(xSegment.Offset, xSegment.Count, ySegment.Offset, ySegment.Count, out var joinedStart)) + { + joinedMemory = xSegment.Array.AsMemory(joinedStart, xSegment.Count + ySegment.Count); + return true; + } + } + + else if (MemoryMarshal.TryGetMemoryManager(x, out MemoryManager? xManager, out xStart, out xLength)) + { + if (MemoryMarshal.TryGetMemoryManager(y, out MemoryManager? yManager, out var yStart, out var yLength) + && ReferenceEquals(xManager, yManager) + && TryGetContiguousStart(xStart, xLength, yStart, yLength, out var joinedStart)) + { + joinedMemory = xManager.Memory.Slice(joinedStart, xLength + yLength); + return true; + } + } + + joinedMemory = default; + return false; + + static bool TryGetContiguousStart(int xStart, int xLength, int yStart, int yLength, out int joinedStart) + { + var xRange = (Start: xStart, Length: xLength); + var yRange = (Start: yStart, Length: yLength); + + var (firstRange, secondRange) = xStart <= yStart ? (xRange, yRange) : (yRange, xRange); + if (firstRange.Start + firstRange.Length == secondRange.Start) + { + joinedStart = firstRange.Start; + return true; + } + + joinedStart = default; + return false; + } + } + + + private static bool CanShapeTogether(TextRunProperties x, TextRunProperties y) + => MathUtilities.AreClose(x.FontRenderingEmSize, y.FontRenderingEmSize) + && x.Typeface == y.Typeface + && x.BaselineAlignment == y.BaselineAlignment; + private static IReadOnlyList ShapeTogether( - IReadOnlyList textRuns, CharacterBufferReference text, int length, TextShaperOptions options) + IReadOnlyList textRuns, ReadOnlyMemory text, TextShaperOptions options) { var shapedRuns = new List(textRuns.Count); - var shapedBuffer = TextShaper.Current.ShapeText(text, length, options); + var shapedBuffer = TextShaper.Current.ShapeText(text, options); for (var i = 0; i < textRuns.Count; i++) { @@ -282,12 +335,14 @@ namespace Avalonia.Media.TextFormatting /// /// The text characters to form from. /// The bidi levels. + /// /// - private static IEnumerable> CoalesceLevels(IReadOnlyList textCharacters, ArraySlice levels) + private static void CoalesceLevels(IReadOnlyList textCharacters, ArraySlice levels, + List processedRuns) { if (levels.Length == 0) { - yield break; + return; } var levelIndex = 0; @@ -295,7 +350,7 @@ namespace Avalonia.Media.TextFormatting TextRunProperties? previousProperties = null; TextCharacters? currentRun = null; - CharacterBufferRange runText = default; + ReadOnlyMemory runText = default; for (var i = 0; i < textCharacters.Count; i++) { @@ -306,18 +361,19 @@ namespace Avalonia.Media.TextFormatting { var drawableRun = textCharacters[i]; - yield return new[] { drawableRun }; + processedRuns.Add(drawableRun); levelIndex += drawableRun.Length; continue; } - runText = new CharacterBufferRange(currentRun.CharacterBufferReference, currentRun.Length); + runText = currentRun.Text; + var runTextSpan = runText.Span; - for (; j < runText.Length;) + for (; j < runTextSpan.Length;) { - Codepoint.ReadAt(runText, j, out var count); + Codepoint.ReadAt(runTextSpan, j, out var count); if (levelIndex + 1 == levels.Length) { @@ -327,9 +383,9 @@ namespace Avalonia.Media.TextFormatting levelIndex++; j += count; - if (j == runText.Length) + if (j == runTextSpan.Length) { - yield return currentRun.GetShapeableCharacters(runText.Take(j), runLevel, ref previousProperties); + processedRuns.AddRange(currentRun.GetShapeableCharacters(runText.Slice(0, j), runLevel, ref previousProperties)); runLevel = levels[levelIndex]; @@ -342,9 +398,10 @@ namespace Avalonia.Media.TextFormatting } // End of this run - yield return currentRun.GetShapeableCharacters(runText.Take(j), runLevel, ref previousProperties); + processedRuns.AddRange(currentRun.GetShapeableCharacters(runText.Slice(0, j), runLevel, ref previousProperties)); - runText = runText.Skip(j); + runText = runText.Slice(j); + runTextSpan = runText.Span; j = 0; @@ -355,10 +412,10 @@ namespace Avalonia.Media.TextFormatting if (currentRun is null || runText.IsEmpty) { - yield break; + return; } - yield return currentRun.GetShapeableCharacters(runText, runLevel, ref previousProperties); + processedRuns.AddRange(currentRun.GetShapeableCharacters(runText, runLevel, ref previousProperties)); } /// @@ -412,7 +469,7 @@ namespace Avalonia.Media.TextFormatting { if (TryGetLineBreak(textCharacters, out var runLineBreak)) { - var splitResult = new TextCharacters(textCharacters.CharacterBufferReference, runLineBreak.PositionWrap, + var splitResult = new TextCharacters(textCharacters.Text.Slice(0, runLineBreak.PositionWrap), textCharacters.Properties); textRuns.Add(splitResult); @@ -443,14 +500,14 @@ namespace Avalonia.Media.TextFormatting { lineBreak = default; - if (textRun.CharacterBufferReference.CharacterBuffer.IsEmpty) + var text = textRun.Text; + + if (text.IsEmpty) { return false; } - var characterBufferRange = new CharacterBufferRange(textRun.CharacterBufferReference, textRun.Length); - - var lineBreakEnumerator = new LineBreakEnumerator(characterBufferRange); + var lineBreakEnumerator = new LineBreakEnumerator(text.Span); while (lineBreakEnumerator.MoveNext()) { @@ -542,8 +599,7 @@ namespace Avalonia.Media.TextFormatting var glyph = glyphTypeface.GetGlyph(s_empty[0]); var glyphInfos = new[] { new GlyphInfo(glyph, firstTextSourceIndex) }; - var characterBufferRange = new CharacterBufferRange(new CharacterBufferReference(s_empty), s_empty.Length); - var shapedBuffer = new ShapedBuffer(characterBufferRange, glyphInfos, glyphTypeface, properties.FontRenderingEmSize, + var shapedBuffer = new ShapedBuffer(s_empty.AsMemory(), glyphInfos, glyphTypeface, properties.FontRenderingEmSize, (sbyte)flowDirection); var textRuns = new List { new ShapedTextRun(shapedBuffer, properties) }; @@ -590,10 +646,8 @@ namespace Avalonia.Media.TextFormatting switch (currentRun) { case ShapedTextRun: - { - var runText = new CharacterBufferRange(currentRun.CharacterBufferReference, currentRun.Length); - - var lineBreaker = new LineBreakEnumerator(runText); + { + var lineBreaker = new LineBreakEnumerator(currentRun.Text.Span); while (lineBreaker.MoveNext()) { @@ -652,9 +706,7 @@ namespace Avalonia.Media.TextFormatting currentRun = textRuns[index]; - runText = new CharacterBufferRange(currentRun.CharacterBufferReference, currentRun.Length); - - lineBreaker = new LineBreakEnumerator(runText); + lineBreaker = new LineBreakEnumerator(currentRun.Text.Span); } } else @@ -772,9 +824,7 @@ namespace Avalonia.Media.TextFormatting var shaperOptions = new TextShaperOptions(glyphTypeface, fontRenderingEmSize, (sbyte)flowDirection, cultureInfo); - var characterBuffer = textRun.CharacterBufferReference; - - var shapedBuffer = textShaper.ShapeText(characterBuffer, textRun.Length, shaperOptions); + var shapedBuffer = textShaper.ShapeText(textRun.Text, shaperOptions); return new ShapedTextRun(shapedBuffer, textRun.Properties); } diff --git a/src/Avalonia.Base/Media/TextFormatting/TextLayout.cs b/src/Avalonia.Base/Media/TextFormatting/TextLayout.cs index ef0c726793..468623b356 100644 --- a/src/Avalonia.Base/Media/TextFormatting/TextLayout.cs +++ b/src/Avalonia.Base/Media/TextFormatting/TextLayout.cs @@ -8,7 +8,7 @@ namespace Avalonia.Media.TextFormatting /// /// Represents a multi line text layout. /// - public class TextLayout + public class TextLayout : IDisposable { private readonly ITextSource _textSource; private readonly TextParagraphProperties _paragraphProperties; @@ -566,5 +566,13 @@ namespace Avalonia.Media.TextFormatting return _textTrimming.CreateCollapsingProperties(new TextCollapsingCreateInfo(width, _paragraphProperties.DefaultTextRunProperties)); } + + public void Dispose() + { + foreach (var line in TextLines) + { + line.Dispose(); + } + } } } diff --git a/src/Avalonia.Base/Media/TextFormatting/TextLine.cs b/src/Avalonia.Base/Media/TextFormatting/TextLine.cs index 61b24dc8c5..3cb26882dc 100644 --- a/src/Avalonia.Base/Media/TextFormatting/TextLine.cs +++ b/src/Avalonia.Base/Media/TextFormatting/TextLine.cs @@ -6,7 +6,7 @@ namespace Avalonia.Media.TextFormatting /// /// Represents a line of text that is used for text rendering. /// - public abstract class TextLine + public abstract class TextLine : IDisposable { /// /// Gets the text runs that are contained within a line. @@ -207,5 +207,7 @@ namespace Avalonia.Media.TextFormatting /// number of characters of the specified range /// an array of bounding rectangles. public abstract IReadOnlyList GetTextBounds(int firstTextSourceCharacterIndex, int textLength); + + public abstract void Dispose(); } } diff --git a/src/Avalonia.Base/Media/TextFormatting/TextLineImpl.cs b/src/Avalonia.Base/Media/TextFormatting/TextLineImpl.cs index a1f93bcd07..ab9686a34a 100644 --- a/src/Avalonia.Base/Media/TextFormatting/TextLineImpl.cs +++ b/src/Avalonia.Base/Media/TextFormatting/TextLineImpl.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Linq; using Avalonia.Utilities; namespace Avalonia.Media.TextFormatting @@ -978,6 +979,17 @@ namespace Avalonia.Media.TextFormatting return GetTextBoundsRightToLeft(firstTextSourceIndex, textLength); } + public override void Dispose() + { + for (int i = 0; i < _textRuns.Count; i++) + { + if (_textRuns[i] is ShapedTextRun shapedTextRun) + { + shapedTextRun.Dispose(); + } + } + } + public TextLineImpl FinalizeLine() { _textLineMetrics = CreateLineMetrics(); @@ -1436,6 +1448,13 @@ namespace Avalonia.Media.TextFormatting var lineHeight = _paragraphProperties.LineHeight; + var lastRunIndex = _textRuns.Count - 1; + + if (lastRunIndex > 0 && _textRuns[lastRunIndex] is TextEndOfLine) + { + lastRunIndex--; + } + for (var index = 0; index < _textRuns.Count; index++) { switch (_textRuns[index]) @@ -1470,7 +1489,7 @@ namespace Avalonia.Media.TextFormatting } } - if (index == _textRuns.Count - 1) + if (index == lastRunIndex) { width = widthIncludingWhitespace + textRun.GlyphRun.Metrics.Width; trailingWhitespaceLength = textRun.GlyphRun.Metrics.TrailingWhitespaceLength; @@ -1490,7 +1509,7 @@ namespace Avalonia.Media.TextFormatting { case FlowDirection.LeftToRight: { - if (index == _textRuns.Count - 1) + if (index == lastRunIndex) { width = widthIncludingWhitespace; trailingWhitespaceLength = 0; @@ -1502,7 +1521,7 @@ namespace Avalonia.Media.TextFormatting case FlowDirection.RightToLeft: { - if (index == _textRuns.Count - 1) + if (index == lastRunIndex) { width = widthIncludingWhitespace; trailingWhitespaceLength = 0; diff --git a/src/Avalonia.Base/Media/TextFormatting/TextRun.cs b/src/Avalonia.Base/Media/TextFormatting/TextRun.cs index 56232ec9c8..da343676bb 100644 --- a/src/Avalonia.Base/Media/TextFormatting/TextRun.cs +++ b/src/Avalonia.Base/Media/TextFormatting/TextRun.cs @@ -1,4 +1,5 @@ -using System.Diagnostics; +using System; +using System.Diagnostics; namespace Avalonia.Media.TextFormatting { @@ -18,7 +19,7 @@ namespace Avalonia.Media.TextFormatting /// /// Gets the text run's text. /// - public virtual CharacterBufferReference CharacterBufferReference => default; + public virtual ReadOnlyMemory Text => default; /// /// A set of properties shared by every characters in the run @@ -34,21 +35,7 @@ namespace Avalonia.Media.TextFormatting _textRun = textRun; } - public string Text - { - get - { - unsafe - { - var characterBuffer = new CharacterBufferRange(_textRun.CharacterBufferReference, _textRun.Length); - - fixed (char* charsPtr = characterBuffer.Span) - { - return new string(charsPtr, 0, _textRun.Length); - } - } - } - } + public string Text => _textRun.Text.ToString(); public TextRunProperties? Properties => _textRun.Properties; } diff --git a/src/Avalonia.Base/Media/TextFormatting/TextShaper.cs b/src/Avalonia.Base/Media/TextFormatting/TextShaper.cs index c161b08d20..ae2a827944 100644 --- a/src/Avalonia.Base/Media/TextFormatting/TextShaper.cs +++ b/src/Avalonia.Base/Media/TextFormatting/TextShaper.cs @@ -40,14 +40,14 @@ namespace Avalonia.Media.TextFormatting } /// - public ShapedBuffer ShapeText(CharacterBufferReference text, int length, TextShaperOptions options = default) + public ShapedBuffer ShapeText(ReadOnlyMemory text, TextShaperOptions options = default) { - return _platformImpl.ShapeText(text, length, options); + return _platformImpl.ShapeText(text, options); } public ShapedBuffer ShapeText(string text, TextShaperOptions options = default) { - return ShapeText(new CharacterBufferReference(text), text.Length, options); + return ShapeText(text.AsMemory(), options); } } } diff --git a/src/Avalonia.Base/Media/TextFormatting/Unicode/BiDiAlgorithm.cs b/src/Avalonia.Base/Media/TextFormatting/Unicode/BiDiAlgorithm.cs index 17ec9b1df2..100d381afe 100644 --- a/src/Avalonia.Base/Media/TextFormatting/Unicode/BiDiAlgorithm.cs +++ b/src/Avalonia.Base/Media/TextFormatting/Unicode/BiDiAlgorithm.cs @@ -6,6 +6,7 @@ using System; using System.Collections.Generic; using System.Runtime.CompilerServices; using System.Threading; +using Avalonia.Collections.Pooled; using Avalonia.Utilities; namespace Avalonia.Media.TextFormatting.Unicode @@ -27,7 +28,7 @@ namespace Avalonia.Media.TextFormatting.Unicode /// as much as possible. /// /// - internal sealed class BidiAlgorithm + internal struct BidiAlgorithm : IDisposable { /// /// The original BiDiClass classes as provided by the caller @@ -66,7 +67,7 @@ namespace Avalonia.Media.TextFormatting.Unicode /// The forward mapping maps the start index to the end index. /// The reverse mapping maps the end index to the start index. /// - private readonly BidiDictionary _isolatePairs = new BidiDictionary(); + private BidiDictionary? _isolatePairs; /// /// The working BiDi classes @@ -118,7 +119,7 @@ namespace Avalonia.Media.TextFormatting.Unicode /// /// A stack of pending isolate openings used by FindIsolatePairs() /// - private readonly Stack _pendingIsolateOpenings = new Stack(); + private Stack? _pendingIsolateOpenings; /// /// The level of the isolating run currently being processed @@ -184,7 +185,7 @@ namespace Avalonia.Media.TextFormatting.Unicode /// /// Initializes a new instance of the class. /// - internal BidiAlgorithm() + public BidiAlgorithm() { } @@ -227,7 +228,7 @@ namespace Avalonia.Media.TextFormatting.Unicode ArraySlice? outLevels) { // Reset state - _isolatePairs.Clear(); + _isolatePairs?.Clear(); _workingClassesBuffer.Clear(); _levelRuns.Clear(); _resolvedLevelsBuffer.Clear(); @@ -323,7 +324,7 @@ namespace Avalonia.Media.TextFormatting.Unicode // Skip isolate pairs // (Because we're working with a slice, we need to adjust the indices // we're using for the isolatePairs map) - if (_isolatePairs.TryGetValue(data.Start + i, out i)) + if (_isolatePairs?.TryGetValue(data.Start + i, out i) == true) { i -= data.Start; } @@ -358,7 +359,7 @@ namespace Avalonia.Media.TextFormatting.Unicode _hasIsolates = false; // BD9... - _pendingIsolateOpenings.Clear(); + _pendingIsolateOpenings?.Clear(); for (var i = 0; i < _originalClasses.Length; i++) { @@ -370,14 +371,16 @@ namespace Avalonia.Media.TextFormatting.Unicode case BidiClass.RightToLeftIsolate: case BidiClass.FirstStrongIsolate: { + _pendingIsolateOpenings ??= new Stack(); _pendingIsolateOpenings.Push(i); _hasIsolates = true; break; } case BidiClass.PopDirectionalIsolate: { - if (_pendingIsolateOpenings.Count > 0) + if (_pendingIsolateOpenings?.Count > 0) { + _isolatePairs ??= new BidiDictionary(); _isolatePairs.Add(_pendingIsolateOpenings.Pop(), i); } @@ -498,7 +501,7 @@ namespace Avalonia.Media.TextFormatting.Unicode if (resolvedIsolate == BidiClass.FirstStrongIsolate) { - if (!_isolatePairs.TryGetValue(i, out var endOfIsolate)) + if (_isolatePairs == null || !_isolatePairs.TryGetValue(i, out var endOfIsolate)) { endOfIsolate = _originalClasses.Length; } @@ -829,7 +832,7 @@ namespace Avalonia.Media.TextFormatting.Unicode var lastCharacterIndex = _isolatedRunMapping[_isolatedRunMapping.Length - 1]; var lastType = _originalClasses[lastCharacterIndex]; if ((lastType == BidiClass.LeftToRightIsolate || lastType == BidiClass.RightToLeftIsolate || lastType == BidiClass.FirstStrongIsolate) && - _isolatePairs.TryGetValue(lastCharacterIndex, out var nextRunIndex)) + _isolatePairs?.TryGetValue(lastCharacterIndex, out var nextRunIndex) == true) { // Find the continuing run index runIndex = FindRunForIndex(nextRunIndex); @@ -1714,5 +1717,13 @@ namespace Avalonia.Media.TextFormatting.Unicode public BidiClass Eos { get; } } + + public void Dispose() + { + _workingClassesBuffer.Dispose(); + _resolvedLevelsBuffer.Dispose(); + _x9Map.Dispose(); + _isolatedRunMapping.Dispose(); + } } } diff --git a/src/Avalonia.Base/Media/TextFormatting/Unicode/BiDiData.cs b/src/Avalonia.Base/Media/TextFormatting/Unicode/BiDiData.cs index 0c51b0898d..0f0b3235e1 100644 --- a/src/Avalonia.Base/Media/TextFormatting/Unicode/BiDiData.cs +++ b/src/Avalonia.Base/Media/TextFormatting/Unicode/BiDiData.cs @@ -11,7 +11,7 @@ namespace Avalonia.Media.TextFormatting.Unicode /// Represents a unicode string and all associated attributes /// for each character required for the bidirectional Unicode algorithm /// - internal class BidiData + internal struct BidiData : IDisposable { private ArrayBuilder _classes; private ArrayBuilder _pairedBracketTypes; @@ -64,7 +64,7 @@ namespace Avalonia.Media.TextFormatting.Unicode /// Appends text to the bidi data. /// /// The text to process. - public void Append(CharacterBufferRange text) + public void Append(ReadOnlySpan text) { _classes.Add(text.Length); _pairedBracketTypes.Add(text.Length); @@ -181,5 +181,15 @@ namespace Avalonia.Media.TextFormatting.Unicode return _tempLevelBuffer.Add(length, false); } + + public void Dispose() + { + _classes.Dispose(); + _pairedBracketTypes.Dispose(); + _pairedBracketValues.Dispose(); + _savedClasses.Dispose(); + _savedPairedBracketTypes.Dispose(); + _tempLevelBuffer.Dispose(); + } } } diff --git a/src/Avalonia.Base/Media/TextFormatting/Unicode/Codepoint.cs b/src/Avalonia.Base/Media/TextFormatting/Unicode/Codepoint.cs index 8b9d1c1d02..22f7b50fd4 100644 --- a/src/Avalonia.Base/Media/TextFormatting/Unicode/Codepoint.cs +++ b/src/Avalonia.Base/Media/TextFormatting/Unicode/Codepoint.cs @@ -166,72 +166,7 @@ namespace Avalonia.Media.TextFormatting.Unicode /// The index to read at. /// The count of character that were read. /// - public static Codepoint ReadAt(IReadOnlyList text, int index, out int count) - { - count = 1; - - if (index >= text.Count) - { - return ReplacementCodepoint; - } - - var code = text[index]; - - ushort hi, low; - - //# High surrogate - if (0xD800 <= code && code <= 0xDBFF) - { - hi = code; - - if (index + 1 == text.Count) - { - return ReplacementCodepoint; - } - - low = text[index + 1]; - - if (0xDC00 <= low && low <= 0xDFFF) - { - count = 2; - return new Codepoint((uint)((hi - 0xD800) * 0x400 + (low - 0xDC00) + 0x10000)); - } - - return ReplacementCodepoint; - } - - //# Low surrogate - if (0xDC00 <= code && code <= 0xDFFF) - { - if (index == 0) - { - return ReplacementCodepoint; - } - - hi = text[index - 1]; - - low = code; - - if (0xD800 <= hi && hi <= 0xDBFF) - { - count = 2; - return new Codepoint((uint)((hi - 0xD800) * 0x400 + (low - 0xDC00) + 0x10000)); - } - - return ReplacementCodepoint; - } - - return new Codepoint(code); - } - - /// - /// Reads the at specified position. - /// - /// The buffer to read from. - /// The index to read at. - /// The count of character that were read. - /// - public static Codepoint ReadAt(CharacterBufferRange text, int index, out int count) + public static Codepoint ReadAt(ReadOnlySpan text, int index, out int count) { count = 1; diff --git a/src/Avalonia.Base/Media/TextFormatting/Unicode/CodepointEnumerator.cs b/src/Avalonia.Base/Media/TextFormatting/Unicode/CodepointEnumerator.cs index a2c36d9a13..d21f30ab7e 100644 --- a/src/Avalonia.Base/Media/TextFormatting/Unicode/CodepointEnumerator.cs +++ b/src/Avalonia.Base/Media/TextFormatting/Unicode/CodepointEnumerator.cs @@ -4,9 +4,9 @@ namespace Avalonia.Media.TextFormatting.Unicode { public ref struct CodepointEnumerator { - private CharacterBufferRange _text; + private ReadOnlySpan _text; - public CodepointEnumerator(CharacterBufferRange text) + public CodepointEnumerator(ReadOnlySpan text) { _text = text; Current = Codepoint.ReplacementCodepoint; @@ -32,7 +32,7 @@ namespace Avalonia.Media.TextFormatting.Unicode Current = Codepoint.ReadAt(_text, 0, out var count); - _text = _text.Skip(count); + _text = _text.Slice(count); return true; } diff --git a/src/Avalonia.Base/Media/TextFormatting/Unicode/Grapheme.cs b/src/Avalonia.Base/Media/TextFormatting/Unicode/Grapheme.cs index f75168083c..fa8e8ac976 100644 --- a/src/Avalonia.Base/Media/TextFormatting/Unicode/Grapheme.cs +++ b/src/Avalonia.Base/Media/TextFormatting/Unicode/Grapheme.cs @@ -7,11 +7,10 @@ namespace Avalonia.Media.TextFormatting.Unicode /// public readonly ref struct Grapheme { - public Grapheme(Codepoint firstCodepoint, int offset, int length) + public Grapheme(Codepoint firstCodepoint, ReadOnlySpan text) { FirstCodepoint = firstCodepoint; - Offset = offset; - Length = length; + Text = text; } /// @@ -20,13 +19,8 @@ namespace Avalonia.Media.TextFormatting.Unicode public Codepoint FirstCodepoint { get; } /// - /// The Offset to the FirstCodepoint + /// The text of the grapheme cluster /// - public int Offset { get; } - - /// - /// The length of the grapheme cluster - /// - public int Length { get; } + public ReadOnlySpan Text { get; } } } diff --git a/src/Avalonia.Base/Media/TextFormatting/Unicode/GraphemeEnumerator.cs b/src/Avalonia.Base/Media/TextFormatting/Unicode/GraphemeEnumerator.cs index dc21e06813..812bb99d99 100644 --- a/src/Avalonia.Base/Media/TextFormatting/Unicode/GraphemeEnumerator.cs +++ b/src/Avalonia.Base/Media/TextFormatting/Unicode/GraphemeEnumerator.cs @@ -3,16 +3,16 @@ // // Licensed to The Avalonia Project under MIT License, courtesy of The .NET Foundation. -using System.Collections.Generic; +using System; using System.Runtime.InteropServices; namespace Avalonia.Media.TextFormatting.Unicode { public ref struct GraphemeEnumerator { - private CharacterBufferRange _text; + private ReadOnlySpan _text; - public GraphemeEnumerator(CharacterBufferRange text) + public GraphemeEnumerator(ReadOnlySpan text) { _text = text; Current = default; @@ -185,9 +185,9 @@ namespace Avalonia.Media.TextFormatting.Unicode Return: - Current = new Grapheme(firstCodepoint, _text.OffsetToFirstChar, processor.CurrentCodeUnitOffset); + Current = new Grapheme(firstCodepoint, _text.Slice(0, processor.CurrentCodeUnitOffset)); - _text = _text.Skip(processor.CurrentCodeUnitOffset); + _text = _text.Slice(processor.CurrentCodeUnitOffset); return true; // rules GB2, GB999 } @@ -195,10 +195,10 @@ namespace Avalonia.Media.TextFormatting.Unicode [StructLayout(LayoutKind.Auto)] private ref struct Processor { - private readonly CharacterBufferRange _buffer; + private readonly ReadOnlySpan _buffer; private int _codeUnitLengthOfCurrentScalar; - internal Processor(CharacterBufferRange buffer) + internal Processor(ReadOnlySpan buffer) { _buffer = buffer; _codeUnitLengthOfCurrentScalar = 0; diff --git a/src/Avalonia.Base/Media/TextFormatting/Unicode/LineBreakEnumerator.cs b/src/Avalonia.Base/Media/TextFormatting/Unicode/LineBreakEnumerator.cs index 41a476c17e..877ab76ce5 100644 --- a/src/Avalonia.Base/Media/TextFormatting/Unicode/LineBreakEnumerator.cs +++ b/src/Avalonia.Base/Media/TextFormatting/Unicode/LineBreakEnumerator.cs @@ -3,7 +3,6 @@ // Ported from: https://github.com/SixLabors/Fonts/ using System; -using System.Collections.Generic; namespace Avalonia.Media.TextFormatting.Unicode { @@ -13,7 +12,7 @@ namespace Avalonia.Media.TextFormatting.Unicode /// public ref struct LineBreakEnumerator { - private readonly IReadOnlyList _text; + private readonly ReadOnlySpan _text; private int _position; private int _lastPosition; private LineBreakClass _currentClass; @@ -29,7 +28,7 @@ namespace Avalonia.Media.TextFormatting.Unicode private int _lb30a; private bool _lb31; - public LineBreakEnumerator(IReadOnlyList text) + public LineBreakEnumerator(ReadOnlySpan text) : this() { _text = text; @@ -63,7 +62,7 @@ namespace Avalonia.Media.TextFormatting.Unicode _lb30a = 0; } - while (_position < _text.Count) + while (_position < _text.Length) { _lastPosition = _position; var lastClass = _nextClass; @@ -93,11 +92,11 @@ namespace Avalonia.Media.TextFormatting.Unicode } } - if (_position >= _text.Count) + if (_position >= _text.Length) { - if (_lastPosition < _text.Count) + if (_lastPosition < _text.Length) { - _lastPosition = _text.Count; + _lastPosition = _text.Length; var required = false; diff --git a/src/Avalonia.Base/Media/TextFormatting/UnshapedTextRun.cs b/src/Avalonia.Base/Media/TextFormatting/UnshapedTextRun.cs index 817086db88..5582ab4787 100644 --- a/src/Avalonia.Base/Media/TextFormatting/UnshapedTextRun.cs +++ b/src/Avalonia.Base/Media/TextFormatting/UnshapedTextRun.cs @@ -1,4 +1,4 @@ -using Avalonia.Utilities; +using System; namespace Avalonia.Media.TextFormatting { @@ -7,52 +7,20 @@ namespace Avalonia.Media.TextFormatting /// public sealed class UnshapedTextRun : TextRun { - public UnshapedTextRun(CharacterBufferReference characterBufferReference, int length, - TextRunProperties properties, sbyte biDiLevel) + public UnshapedTextRun(ReadOnlyMemory text, TextRunProperties properties, sbyte biDiLevel) { - CharacterBufferReference = characterBufferReference; - Length = length; + Text = text; Properties = properties; BidiLevel = biDiLevel; } - public override int Length { get; } + public override int Length + => Text.Length; - public override CharacterBufferReference CharacterBufferReference { get; } + public override ReadOnlyMemory Text { get; } public override TextRunProperties Properties { get; } public sbyte BidiLevel { get; } - - public bool CanShapeTogether(UnshapedTextRun unshapedTextRun) - { - if (!CharacterBufferReference.Equals(unshapedTextRun.CharacterBufferReference)) - { - return false; - } - - if (BidiLevel != unshapedTextRun.BidiLevel) - { - return false; - } - - if (!MathUtilities.AreClose(Properties.FontRenderingEmSize, - unshapedTextRun.Properties.FontRenderingEmSize)) - { - return false; - } - - if (Properties.Typeface != unshapedTextRun.Properties.Typeface) - { - return false; - } - - if (Properties.BaselineAlignment != unshapedTextRun.Properties.BaselineAlignment) - { - return false; - } - - return true; - } } } diff --git a/src/Avalonia.Base/Platform/DefaultPlatformSettings.cs b/src/Avalonia.Base/Platform/DefaultPlatformSettings.cs index d0b27b057f..b5e7298b7e 100644 --- a/src/Avalonia.Base/Platform/DefaultPlatformSettings.cs +++ b/src/Avalonia.Base/Platform/DefaultPlatformSettings.cs @@ -1,15 +1,15 @@ using System; using Avalonia.Input; +using Avalonia.Media; namespace Avalonia.Platform { /// - /// A default implementation of for platforms which don't have - /// an OS-specific implementation. + /// A default implementation of for platforms. /// public class DefaultPlatformSettings : IPlatformSettings { - public Size GetTapSize(PointerType type) + public virtual Size GetTapSize(PointerType type) { return type switch { @@ -17,7 +17,7 @@ namespace Avalonia.Platform _ => new(4, 4), }; } - public Size GetDoubleTapSize(PointerType type) + public virtual Size GetDoubleTapSize(PointerType type) { return type switch { @@ -25,8 +25,23 @@ namespace Avalonia.Platform _ => new(4, 4), }; } - public TimeSpan GetDoubleTapTime(PointerType type) => TimeSpan.FromMilliseconds(500); + public virtual TimeSpan GetDoubleTapTime(PointerType type) => TimeSpan.FromMilliseconds(500); - public TimeSpan HoldWaitDuration { get; set; } = TimeSpan.FromMilliseconds(300); + public virtual TimeSpan HoldWaitDuration => TimeSpan.FromMilliseconds(300); + + public virtual PlatformColorValues GetColorValues() + { + return new PlatformColorValues + { + ThemeVariant = PlatformThemeVariant.Light + }; + } + + public event EventHandler? ColorValuesChanged; + + protected void OnColorValuesChanged(PlatformColorValues colorValues) + { + ColorValuesChanged?.Invoke(this, colorValues); + } } } diff --git a/src/Avalonia.Base/Platform/IGlyphRunImpl.cs b/src/Avalonia.Base/Platform/IGlyphRunImpl.cs index 7801bdd50f..6a8ae4d954 100644 --- a/src/Avalonia.Base/Platform/IGlyphRunImpl.cs +++ b/src/Avalonia.Base/Platform/IGlyphRunImpl.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Generic; using Avalonia.Metadata; namespace Avalonia.Platform @@ -7,5 +8,8 @@ namespace Avalonia.Platform /// Actual implementation of a glyph run that stores platform dependent resources. /// [Unstable] - public interface IGlyphRunImpl : IDisposable { } + public interface IGlyphRunImpl : IDisposable + { + IReadOnlyList GetIntersections(float lowerBound, float upperBound); + } } diff --git a/src/Avalonia.Base/Platform/IPlatformSettings.cs b/src/Avalonia.Base/Platform/IPlatformSettings.cs index 7c4c1420eb..b167cf0edc 100644 --- a/src/Avalonia.Base/Platform/IPlatformSettings.cs +++ b/src/Avalonia.Base/Platform/IPlatformSettings.cs @@ -28,6 +28,19 @@ namespace Avalonia.Platform /// TimeSpan GetDoubleTapTime(PointerType type); - TimeSpan HoldWaitDuration { get; set; } + /// + /// Holding duration between pointer press and when event is fired. + /// + TimeSpan HoldWaitDuration { get; } + + /// + /// Gets current system color values including dark mode and accent colors. + /// + PlatformColorValues GetColorValues(); + + /// + /// Raises when current system color values are changed. Including changing of a dark mode and accent colors. + /// + event EventHandler? ColorValuesChanged; } } diff --git a/src/Avalonia.Base/Platform/ITextShaperImpl.cs b/src/Avalonia.Base/Platform/ITextShaperImpl.cs index c3eb89ab1a..a651b49e64 100644 --- a/src/Avalonia.Base/Platform/ITextShaperImpl.cs +++ b/src/Avalonia.Base/Platform/ITextShaperImpl.cs @@ -1,4 +1,5 @@ -using Avalonia.Media.TextFormatting; +using System; +using Avalonia.Media.TextFormatting; using Avalonia.Metadata; namespace Avalonia.Platform @@ -13,9 +14,8 @@ namespace Avalonia.Platform /// Shapes the specified region within the text and returns a shaped buffer. /// /// The text buffer. - /// The length of text. /// Text shaper options to customize the shaping process. /// A shaped glyph run. - ShapedBuffer ShapeText(CharacterBufferReference text, int length, TextShaperOptions options); + ShapedBuffer ShapeText(ReadOnlyMemory text, TextShaperOptions options); } } diff --git a/src/Avalonia.Base/Platform/Internal/AssemblyDescriptor.cs b/src/Avalonia.Base/Platform/Internal/AssemblyDescriptor.cs index df2a26ddd3..467cd530fc 100644 --- a/src/Avalonia.Base/Platform/Internal/AssemblyDescriptor.cs +++ b/src/Avalonia.Base/Platform/Internal/AssemblyDescriptor.cs @@ -32,7 +32,7 @@ internal class AssemblyDescriptor : IAssemblyDescriptor Resources.Remove(Constants.AvaloniaResourceName); var indexLength = new BinaryReader(resources).ReadInt32(); - var index = AvaloniaResourcesIndexReaderWriter.Read(new SlicedStream(resources, 4, indexLength)); + var index = AvaloniaResourcesIndexReaderWriter.ReadIndex(new SlicedStream(resources, 4, indexLength)); var baseOffset = indexLength + 4; AvaloniaResources = index.ToDictionary(r => GetPathRooted(r), r => (IAssetDescriptor) new AvaloniaResourceDescriptor(assembly, baseOffset + r.Offset, r.Size)); diff --git a/src/Avalonia.Base/Platform/PlatformColorValues.cs b/src/Avalonia.Base/Platform/PlatformColorValues.cs new file mode 100644 index 0000000000..d14b58e939 --- /dev/null +++ b/src/Avalonia.Base/Platform/PlatformColorValues.cs @@ -0,0 +1,68 @@ +using Avalonia.Media; + +namespace Avalonia.Platform; + +/// +/// System theme variant or mode. +/// +public enum PlatformThemeVariant +{ + Light, + Dark +} + +/// +/// System high contrast preference. +/// +public enum ColorContrastPreference +{ + NoPreference, + High +} + +/// +/// Information about current system color values, including information about dark mode and accent colors. +/// +public record PlatformColorValues +{ + private static Color DefaultAccent => new(255, 0, 120, 215); + private Color _accentColor2, _accentColor3; + + /// + /// System theme variant or mode. + /// + public PlatformThemeVariant ThemeVariant { get; init; } + + /// + /// System high contrast preference. + /// + public ColorContrastPreference ContrastPreference { get; init; } + + /// + /// Primary system accent color. + /// + public Color AccentColor1 { get; init; } + + /// + /// Secondary system accent color. On some platforms can return the same value as . + /// + public Color AccentColor2 + { + get => _accentColor2 != default ? _accentColor2 : AccentColor1; + init => _accentColor2 = value; + } + + /// + /// Tertiary system accent color. On some platforms can return the same value as . + /// + public Color AccentColor3 + { + get => _accentColor3 != default ? _accentColor3 : AccentColor1; + init => _accentColor3 = value; + } + + public PlatformColorValues() + { + AccentColor1 = DefaultAccent; + } +} diff --git a/src/Avalonia.Base/Platform/SystemNavigationManager.cs b/src/Avalonia.Base/Platform/SystemNavigationManager.cs new file mode 100644 index 0000000000..6165b2bb77 --- /dev/null +++ b/src/Avalonia.Base/Platform/SystemNavigationManager.cs @@ -0,0 +1,18 @@ +using System; +using Avalonia.Interactivity; +using Avalonia.Metadata; + +namespace Avalonia.Platform +{ + [Unstable] + public interface ITopLevelWithSystemNavigationManager + { + ISystemNavigationManager SystemNavigationManager { get; } + } + + [Unstable] + public interface ISystemNavigationManager + { + public event EventHandler? BackRequested; + } +} diff --git a/src/Avalonia.Base/Utilities/ArrayBuilder.cs b/src/Avalonia.Base/Utilities/ArrayBuilder.cs index 60bf2c7586..e6b67bd383 100644 --- a/src/Avalonia.Base/Utilities/ArrayBuilder.cs +++ b/src/Avalonia.Base/Utilities/ArrayBuilder.cs @@ -3,6 +3,7 @@ // Ported from: https://github.com/SixLabors/Fonts/ using System; +using System.Buffers; using System.Runtime.CompilerServices; namespace Avalonia.Utilities @@ -11,7 +12,7 @@ namespace Avalonia.Utilities /// A helper type for avoiding allocations while building arrays. /// /// The type of item contained in the array. - internal struct ArrayBuilder + internal struct ArrayBuilder : IDisposable where T : struct { private const int DefaultCapacity = 4; @@ -135,7 +136,7 @@ namespace Avalonia.Utilities } // Same expansion algorithm as List. - var newCapacity = length == 0 ? DefaultCapacity : (uint)length * 2u; + var newCapacity = length == 0 ? DefaultCapacity : length * 2; if (newCapacity > MaxCoreClrArrayLength) { @@ -144,14 +145,15 @@ namespace Avalonia.Utilities if (newCapacity < min) { - newCapacity = (uint)min; + newCapacity = min; } - - var array = new T[newCapacity]; + + var array = ArrayPool.Shared.Rent(newCapacity); if (_size > 0) { Array.Copy(_data!, array, _size); + ArrayPool.Shared.Return(_data!); } _data = array; @@ -180,5 +182,13 @@ namespace Avalonia.Utilities /// The . [MethodImpl(MethodImplOptions.AggressiveInlining)] public ArraySlice AsSlice(int start, int length) => new ArraySlice(_data!, start, length); + + public void Dispose() + { + if (_data != null) + { + ArrayPool.Shared.Return(_data); + } + } } } diff --git a/src/Avalonia.Base/Utilities/ArraySlice.cs b/src/Avalonia.Base/Utilities/ArraySlice.cs index 39c0cd5556..b70088a907 100644 --- a/src/Avalonia.Base/Utilities/ArraySlice.cs +++ b/src/Avalonia.Base/Utilities/ArraySlice.cs @@ -3,6 +3,7 @@ // Ported from: https://github.com/SixLabors/Fonts/ using System; +using System.Buffers; using System.Collections; using System.Collections.Generic; using System.Runtime.CompilerServices; @@ -185,5 +186,13 @@ namespace Avalonia.Utilities /// int IReadOnlyCollection.Count => Length; + + public void ReturnRent() + { + if (_data != null) + { + ArrayPool.Shared.Return(_data); + } + } } } diff --git a/src/Avalonia.Base/Utilities/AvaloniaResourcesIndex.cs b/src/Avalonia.Base/Utilities/AvaloniaResourcesIndex.cs index 5235e23e08..3c7e82f080 100644 --- a/src/Avalonia.Base/Utilities/AvaloniaResourcesIndex.cs +++ b/src/Avalonia.Base/Utilities/AvaloniaResourcesIndex.cs @@ -1,105 +1,116 @@ using System; using System.Collections.Generic; using System.IO; -using System.Runtime.Serialization; -using System.Xml.Linq; -using System.Linq; -using System.Diagnostics.CodeAnalysis; +using System.Text; namespace Avalonia.Utilities { - #if !BUILDTASK +#if !BUILDTASK public - #endif +#endif static class AvaloniaResourcesIndexReaderWriter { - private const int LastKnownVersion = 1; - public static List Read(Stream stream) + private const int XmlLegacyVersion = 1; + private const int BinaryCurrentVersion = 2; + + public static List ReadIndex(Stream stream) { - var ver = new BinaryReader(stream).ReadInt32(); - if (ver > LastKnownVersion) - throw new Exception("Resources index format version is not known"); - - var assetDoc = XDocument.Load(stream); - XNamespace assetNs = assetDoc.Root!.Attribute("xmlns")!.Value; - List entries = - (from entry in assetDoc.Root.Element(assetNs + "Entries")!.Elements(assetNs + "AvaloniaResourcesIndexEntry") - select new AvaloniaResourcesIndexEntry - { - Path = entry.Element(assetNs + "Path")!.Value, - Offset = int.Parse(entry.Element(assetNs + "Offset")!.Value), - Size = int.Parse(entry.Element(assetNs + "Size")!.Value) - }).ToList(); + using var reader = new BinaryReader(stream, Encoding.UTF8, leaveOpen: true); - return entries; + var version = reader.ReadInt32(); + return version switch + { + XmlLegacyVersion => ReadXmlIndex(), + BinaryCurrentVersion => ReadBinaryIndex(reader), + _ => throw new Exception($"Unknown resources index format version {version}") + }; } - [RequiresUnreferencedCode("AvaloniaResources uses Data Contract Serialization, which might require unreferenced code")] - public static void Write(Stream stream, List entries) + private static List ReadXmlIndex() + => throw new NotSupportedException("Found legacy resources index format: please recompile your XAML files"); + + private static List ReadBinaryIndex(BinaryReader reader) { - new BinaryWriter(stream).Write(LastKnownVersion); - new DataContractSerializer(typeof(AvaloniaResourcesIndex)).WriteObject(stream, - new AvaloniaResourcesIndex() - { - Entries = entries + var entryCount = reader.ReadInt32(); + var entries = new List(entryCount); + + for (var i = 0; i < entryCount; ++i) + { + entries.Add(new AvaloniaResourcesIndexEntry { + Path = reader.ReadString(), + Offset = reader.ReadInt32(), + Size = reader.ReadInt32() }); + } + + return entries; } - [RequiresUnreferencedCode("AvaloniaResources uses Data Contract Serialization, which might require unreferenced code")] - public static byte[] Create(Dictionary data) + public static void WriteIndex(Stream output, List entries) { - var sources = data.ToList(); - var offsets = new Dictionary(); - var coffset = 0; - foreach (var s in sources) + using var writer = new BinaryWriter(output, Encoding.UTF8, leaveOpen: true); + + WriteIndex(writer, entries); + } + + private static void WriteIndex(BinaryWriter writer, List entries) + { + writer.Write(BinaryCurrentVersion); + writer.Write(entries.Count); + + foreach (var entry in entries) { - offsets[s.Key] = coffset; - coffset += s.Value.Length; + writer.Write(entry.Path ?? string.Empty); + writer.Write(entry.Offset); + writer.Write(entry.Size); } - var index = sources.Select(s => new AvaloniaResourcesIndexEntry - { - Path = s.Key, - Size = s.Value.Length, - Offset = offsets[s.Key] - }).ToList(); - var output = new MemoryStream(); - var ms = new MemoryStream(); - AvaloniaResourcesIndexReaderWriter.Write(ms, index); - new BinaryWriter(output).Write((int)ms.Length); - ms.Position = 0; - ms.CopyTo(output); - foreach (var s in sources) + } + + public static void WriteResources(Stream output, List<(string Path, int Size, Func Open)> resources) + { + var entries = new List(resources.Count); + var offset = 0; + + foreach (var resource in resources) { - output.Write(s.Value,0,s.Value.Length); + entries.Add(new AvaloniaResourcesIndexEntry + { + Path = resource.Path, + Offset = offset, + Size = resource.Size + }); + offset += resource.Size; } - return output.ToArray(); - } - } + using var writer = new BinaryWriter(output, Encoding.UTF8, leaveOpen: true); + writer.Write(0); // index size placeholder, overwritten below - [DataContract] -#if !BUILDTASK - public -#endif - class AvaloniaResourcesIndex - { - [DataMember] - public List Entries { get; set; } = new List(); + var posBeforeEntries = output.Position; + WriteIndex(writer, entries); + + var posAfterEntries = output.Position; + var indexSize = (int) (posAfterEntries - posBeforeEntries); + output.Position = 0L; + writer.Write(indexSize); + output.Position = posAfterEntries; + + foreach (var resource in resources) + { + using var resourceStream = resource.Open(); + resourceStream.CopyTo(output); + } + } } - [DataContract] #if !BUILDTASK public #endif class AvaloniaResourcesIndexEntry { - [DataMember] public string? Path { get; set; } - - [DataMember] + public int Offset { get; set; } - - [DataMember] + public int Size { get; set; } } } diff --git a/src/Avalonia.Build.Tasks/Avalonia.Build.Tasks.csproj b/src/Avalonia.Build.Tasks/Avalonia.Build.Tasks.csproj index 35b22f4f23..e44b7290af 100644 --- a/src/Avalonia.Build.Tasks/Avalonia.Build.Tasks.csproj +++ b/src/Avalonia.Build.Tasks/Avalonia.Build.Tasks.csproj @@ -18,6 +18,9 @@ Shared/AvaloniaResourcesIndex.cs + + Shared/Constants.cs + Shared/AvaloniaResourceXamlInfo.cs diff --git a/src/Avalonia.Build.Tasks/CompileAvaloniaXamlTask.cs b/src/Avalonia.Build.Tasks/CompileAvaloniaXamlTask.cs index fd07e0a143..3bd1ce5ce7 100644 --- a/src/Avalonia.Build.Tasks/CompileAvaloniaXamlTask.cs +++ b/src/Avalonia.Build.Tasks/CompileAvaloniaXamlTask.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Generic; using System.IO; using System.Linq; using Microsoft.Build.Framework; @@ -10,9 +11,10 @@ namespace Avalonia.Build.Tasks public bool Execute() { Enum.TryParse(ReportImportance, true, out MessageImportance outputImportance); + var writtenFilePaths = new List(); - OutputPath = OutputPath ?? AssemblyFile; - RefOutputPath = RefOutputPath ?? RefAssemblyFile; + OutputPath ??= AssemblyFile; + RefOutputPath ??= RefAssemblyFile; var outputPdb = GetPdbPath(OutputPath); var input = AssemblyFile; var refInput = RefOutputPath; @@ -21,8 +23,9 @@ namespace Avalonia.Build.Tasks if (OriginalCopyPath != null) { var originalCopyPathRef = Path.ChangeExtension(OriginalCopyPath, ".ref.dll"); - + File.Copy(AssemblyFile, OriginalCopyPath, true); + writtenFilePaths.Add(OriginalCopyPath); input = OriginalCopyPath; File.Delete(AssemblyFile); @@ -30,6 +33,7 @@ namespace Avalonia.Build.Tasks { var copyPdb = GetPdbPath(OriginalCopyPath); File.Copy(inputPdb, copyPdb, true); + writtenFilePaths.Add(copyPdb); File.Delete(inputPdb); inputPdb = copyPdb; } @@ -39,6 +43,7 @@ namespace Avalonia.Build.Tasks // We also copy ref assembly just for case if needed later for testing. // But do not remove the original one, as MSBuild actually complains about it with multi-thread compiling. File.Copy(RefAssemblyFile, originalCopyPathRef, true); + writtenFilePaths.Add(originalCopyPathRef); refInput = originalCopyPathRef; } } @@ -53,13 +58,25 @@ namespace Avalonia.Build.Tasks ProjectDirectory, VerifyIl, DefaultCompileBindings, outputImportance, (SignAssembly && !DelaySign) ? AssemblyOriginatorKeyFile : null, SkipXamlCompilation, DebuggerLaunch); if (!res.Success) + { + WrittenFilePaths = writtenFilePaths.ToArray(); return false; + } + if (!res.WrittenFile) { File.Copy(input, OutputPath, true); - if(File.Exists(inputPdb)) + if (File.Exists(inputPdb)) File.Copy(inputPdb, outputPdb, true); } + else if (!string.IsNullOrWhiteSpace(RefOutputPath) && File.Exists(RefOutputPath)) + writtenFilePaths.Add(RefOutputPath); + + writtenFilePaths.Add(OutputPath); + if (File.Exists(outputPdb)) + writtenFilePaths.Add(outputPdb); + + WrittenFilePaths = writtenFilePaths.ToArray(); return true; } @@ -103,5 +120,8 @@ namespace Avalonia.Build.Tasks public ITaskHost HostObject { get; set; } public bool DebuggerLaunch { get; set; } + + [Output] + public string[] WrittenFilePaths { get; private set; } = Array.Empty(); } } diff --git a/src/Avalonia.Build.Tasks/GenerateAvaloniaResourcesTask.cs b/src/Avalonia.Build.Tasks/GenerateAvaloniaResourcesTask.cs index 65681a2d28..264a86b014 100644 --- a/src/Avalonia.Build.Tasks/GenerateAvaloniaResourcesTask.cs +++ b/src/Avalonia.Build.Tasks/GenerateAvaloniaResourcesTask.cs @@ -77,29 +77,9 @@ namespace Avalonia.Build.Tasks private void Pack(Stream output, List sources) { - var offsets = new Dictionary(); - var coffset = 0; - foreach (var s in sources) - { - offsets[s] = coffset; - coffset += s.Size; - } - var index = sources.Select(s => new AvaloniaResourcesIndexEntry - { - Path = s.Path, - Size = s.Size, - Offset = offsets[s] - }).ToList(); - var ms = new MemoryStream(); - AvaloniaResourcesIndexReaderWriter.Write(ms, index); - new BinaryWriter(output).Write((int)ms.Length); - ms.Position = 0; - ms.CopyTo(output); - foreach (var s in sources) - { - using (var input = s.Open()) - input.CopyTo(output); - } + AvaloniaResourcesIndexReaderWriter.WriteResources( + output, + sources.Select(source => (source.Path, source.Size, (Func) source.Open)).ToList()); } private bool PreProcessXamlFiles(List sources) diff --git a/src/Avalonia.Build.Tasks/XamlCompilerTaskExecutor.Helpers.cs b/src/Avalonia.Build.Tasks/XamlCompilerTaskExecutor.Helpers.cs index f83e07bd74..079ea91db1 100644 --- a/src/Avalonia.Build.Tasks/XamlCompilerTaskExecutor.Helpers.cs +++ b/src/Avalonia.Build.Tasks/XamlCompilerTaskExecutor.Helpers.cs @@ -1,6 +1,8 @@ +using System; using System.Collections.Generic; using System.IO; using System.Linq; +using Avalonia.Platform.Internal; using Avalonia.Utilities; using Mono.Cecil; using Mono.Cecil.Cil; @@ -34,13 +36,13 @@ namespace Avalonia.Build.Tasks { _asm = asm; _embedded = ((EmbeddedResource)asm.MainModule.Resources.FirstOrDefault(r => - r.ResourceType == ResourceType.Embedded && r.Name == "!AvaloniaResources")); + r.ResourceType == ResourceType.Embedded && r.Name == Constants.AvaloniaResourceName)); if (_embedded == null) return; using (var stream = _embedded.GetResourceStream()) { var br = new BinaryReader(stream); - var index = AvaloniaResourcesIndexReaderWriter.Read(new MemoryStream(br.ReadBytes(br.ReadInt32()))); + var index = AvaloniaResourcesIndexReaderWriter.ReadIndex(new MemoryStream(br.ReadBytes(br.ReadInt32()))); var baseOffset = stream.Position; foreach (var e in index) { @@ -61,9 +63,18 @@ namespace Avalonia.Build.Tasks if (_resources.Count == 0) return; - _embedded = new EmbeddedResource("!AvaloniaResources", ManifestResourceAttributes.Public, - AvaloniaResourcesIndexReaderWriter.Create(_resources.ToDictionary(x => x.Key, - x => x.Value.FileContents))); + var output = new MemoryStream(); + + AvaloniaResourcesIndexReaderWriter.WriteResources( + output, + _resources.Select(x => ( + Path: x.Key, + Size: x.Value.FileContents.Length, + Open: (Func) (() => new MemoryStream(x.Value.FileContents)) + )).ToList()); + + output.Position = 0L; + _embedded = new EmbeddedResource(Constants.AvaloniaResourceName, ManifestResourceAttributes.Public, output); _asm.MainModule.Resources.Add(_embedded); } diff --git a/src/Avalonia.Controls.DataGrid/Utils/CellEditBinding.cs b/src/Avalonia.Controls.DataGrid/Utils/CellEditBinding.cs index d6c46bb1e0..01badf3833 100644 --- a/src/Avalonia.Controls.DataGrid/Utils/CellEditBinding.cs +++ b/src/Avalonia.Controls.DataGrid/Utils/CellEditBinding.cs @@ -2,7 +2,6 @@ using Avalonia.Reactive; using System; using System.Collections.Generic; -using Avalonia.Reactive; namespace Avalonia.Controls.Utils { diff --git a/src/Avalonia.Controls/ComboBox.cs b/src/Avalonia.Controls/ComboBox.cs index e3466d24ae..aeaf94d728 100644 --- a/src/Avalonia.Controls/ComboBox.cs +++ b/src/Avalonia.Controls/ComboBox.cs @@ -361,7 +361,7 @@ namespace Avalonia.Controls _subscriptionsOnOpen.Clear(); - var toplevel = this.GetVisualRoot() as TopLevel; + var toplevel = TopLevel.GetTopLevel(this); if (toplevel != null) { toplevel.AddDisposableHandler(PointerWheelChangedEvent, (s, ev) => diff --git a/src/Avalonia.Controls/Embedding/Offscreen/OffscreenTopLevelImpl.cs b/src/Avalonia.Controls/Embedding/Offscreen/OffscreenTopLevelImpl.cs index 4a227a0c00..64f96b6987 100644 --- a/src/Avalonia.Controls/Embedding/Offscreen/OffscreenTopLevelImpl.cs +++ b/src/Avalonia.Controls/Embedding/Offscreen/OffscreenTopLevelImpl.cs @@ -58,6 +58,8 @@ namespace Avalonia.Controls.Embedding.Offscreen public Action? TransparencyLevelChanged { get; set; } + public void SetFrameThemeVariant(PlatformThemeVariant themeVariant) { } + /// public AcrylicPlatformCompensationLevels AcrylicCompensationLevels { get; } = new AcrylicPlatformCompensationLevels(1, 1, 1); diff --git a/src/Avalonia.Controls/Platform/ITopLevelImpl.cs b/src/Avalonia.Controls/Platform/ITopLevelImpl.cs index 6bb94c6070..bf74e0f8f4 100644 --- a/src/Avalonia.Controls/Platform/ITopLevelImpl.cs +++ b/src/Avalonia.Controls/Platform/ITopLevelImpl.cs @@ -163,6 +163,12 @@ namespace Avalonia.Platform /// WindowTransparencyLevel TransparencyLevel { get; } + /// + /// Sets the on the frame if it should be dark or light. + /// Also applies for the mobile status bar. + /// + void SetFrameThemeVariant(PlatformThemeVariant themeVariant); + /// /// Gets the for the platform. /// diff --git a/src/Avalonia.Controls/Presenters/ScrollContentPresenter.cs b/src/Avalonia.Controls/Presenters/ScrollContentPresenter.cs index 5a691ce665..0cfe4bada1 100644 --- a/src/Avalonia.Controls/Presenters/ScrollContentPresenter.cs +++ b/src/Avalonia.Controls/Presenters/ScrollContentPresenter.cs @@ -428,6 +428,8 @@ namespace Avalonia.Controls.Presenters Offset = newOffset; e.Handled = !IsScrollChainingEnabled || offsetChanged; + + e.ShouldEndScrollGesture = !IsScrollChainingEnabled && !offsetChanged; } } diff --git a/src/Avalonia.Controls/Presenters/TextPresenter.cs b/src/Avalonia.Controls/Presenters/TextPresenter.cs index 480a8ee7a7..f599511392 100644 --- a/src/Avalonia.Controls/Presenters/TextPresenter.cs +++ b/src/Avalonia.Controls/Presenters/TextPresenter.cs @@ -588,6 +588,7 @@ namespace Avalonia.Controls.Presenters protected virtual void InvalidateTextLayout() { + _textLayout?.Dispose(); _textLayout = null; InvalidateMeasure(); @@ -597,6 +598,7 @@ namespace Avalonia.Controls.Presenters { _constraint = availableSize; + _textLayout?.Dispose(); _textLayout = null; InvalidateArrange(); @@ -622,6 +624,7 @@ namespace Avalonia.Controls.Presenters _constraint = new Size(Math.Ceiling(finalSize.Width), double.PositiveInfinity); + _textLayout?.Dispose(); _textLayout = null; return finalSize; diff --git a/src/Avalonia.Controls/Primitives/OverlayPopupHost.cs b/src/Avalonia.Controls/Primitives/OverlayPopupHost.cs index e0f72cae54..e265f4eb6a 100644 --- a/src/Avalonia.Controls/Primitives/OverlayPopupHost.cs +++ b/src/Avalonia.Controls/Primitives/OverlayPopupHost.cs @@ -123,7 +123,7 @@ namespace Avalonia.Controls.Primitives public static IPopupHost CreatePopupHost(Visual target, IAvaloniaDependencyResolver? dependencyResolver) { - var platform = (target.GetVisualRoot() as TopLevel)?.PlatformImpl?.CreatePopup(); + var platform = TopLevel.GetTopLevel(target)?.PlatformImpl?.CreatePopup(); if (platform != null) return new PopupRoot((TopLevel)target.GetVisualRoot()!, platform, dependencyResolver); diff --git a/src/Avalonia.Controls/Primitives/Popup.cs b/src/Avalonia.Controls/Primitives/Popup.cs index bf79198939..7cac12eabe 100644 --- a/src/Avalonia.Controls/Primitives/Popup.cs +++ b/src/Avalonia.Controls/Primitives/Popup.cs @@ -358,7 +358,7 @@ namespace Avalonia.Controls.Primitives return; } - var topLevel = placementTarget.VisualRoot as TopLevel; + var topLevel = TopLevel.GetTopLevel(placementTarget); if (topLevel == null) { diff --git a/src/Avalonia.Controls/Primitives/ToggleButton.cs b/src/Avalonia.Controls/Primitives/ToggleButton.cs index 148797c53a..dfb436a55e 100644 --- a/src/Avalonia.Controls/Primitives/ToggleButton.cs +++ b/src/Avalonia.Controls/Primitives/ToggleButton.cs @@ -32,26 +32,42 @@ namespace Avalonia.Controls.Primitives /// /// Defines the event. /// + [Obsolete("Use IsCheckedChangedEvent instead.")] public static readonly RoutedEvent CheckedEvent = - RoutedEvent.Register(nameof(Checked), RoutingStrategies.Bubble); + RoutedEvent.Register( + nameof(Checked), + RoutingStrategies.Bubble); /// /// Defines the event. /// + [Obsolete("Use IsCheckedChangedEvent instead.")] public static readonly RoutedEvent UncheckedEvent = - RoutedEvent.Register(nameof(Unchecked), RoutingStrategies.Bubble); + RoutedEvent.Register( + nameof(Unchecked), + RoutingStrategies.Bubble); /// /// Defines the event. /// + [Obsolete("Use IsCheckedChangedEvent instead.")] public static readonly RoutedEvent IndeterminateEvent = - RoutedEvent.Register(nameof(Indeterminate), RoutingStrategies.Bubble); + RoutedEvent.Register( + nameof(Indeterminate), + RoutingStrategies.Bubble); + + /// + /// Defines the event. + /// + public static readonly RoutedEvent IsCheckedChangedEvent = + RoutedEvent.Register( + nameof(IsCheckedChanged), + RoutingStrategies.Bubble); private bool? _isChecked = false; static ToggleButton() { - IsCheckedProperty.Changed.AddClassHandler((x, e) => x.OnIsCheckedChanged(e)); } public ToggleButton() @@ -62,6 +78,7 @@ namespace Avalonia.Controls.Primitives /// /// Raised when a is checked. /// + [Obsolete("Use IsCheckedChanged instead.")] public event EventHandler? Checked { add => AddHandler(CheckedEvent, value); @@ -71,6 +88,7 @@ namespace Avalonia.Controls.Primitives /// /// Raised when a is unchecked. /// + [Obsolete("Use IsCheckedChanged instead.")] public event EventHandler? Unchecked { add => AddHandler(UncheckedEvent, value); @@ -80,12 +98,22 @@ namespace Avalonia.Controls.Primitives /// /// Raised when a is neither checked nor unchecked. /// + [Obsolete("Use IsCheckedChanged instead.")] public event EventHandler? Indeterminate { add => AddHandler(IndeterminateEvent, value); remove => RemoveHandler(IndeterminateEvent, value); } + /// + /// Raised when the property value changes. + /// + public event EventHandler? IsCheckedChanged + { + add => AddHandler(IsCheckedChangedEvent, value); + remove => RemoveHandler(IsCheckedChangedEvent, value); + } + /// /// Gets or sets whether the is checked. /// @@ -147,6 +175,7 @@ namespace Avalonia.Controls.Primitives /// Called when becomes true. /// /// Event arguments for the routed event that is raised by the default implementation of this method. + [Obsolete("Use OnIsCheckedChanged instead.")] protected virtual void OnChecked(RoutedEventArgs e) { RaiseEvent(e); @@ -156,6 +185,7 @@ namespace Avalonia.Controls.Primitives /// Called when becomes false. /// /// Event arguments for the routed event that is raised by the default implementation of this method. + [Obsolete("Use OnIsCheckedChanged instead.")] protected virtual void OnUnchecked(RoutedEventArgs e) { RaiseEvent(e); @@ -165,31 +195,51 @@ namespace Avalonia.Controls.Primitives /// Called when becomes null. /// /// Event arguments for the routed event that is raised by the default implementation of this method. + [Obsolete("Use OnIsCheckedChanged instead.")] protected virtual void OnIndeterminate(RoutedEventArgs e) { RaiseEvent(e); } + /// + /// Called when changes. + /// + /// Event arguments for the routed event that is raised by the default implementation of this method. + protected virtual void OnIsCheckedChanged(RoutedEventArgs e) + { + RaiseEvent(e); + } + protected override AutomationPeer OnCreateAutomationPeer() { return new ToggleButtonAutomationPeer(this); } - private void OnIsCheckedChanged(AvaloniaPropertyChangedEventArgs e) + /// + protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change) { - var newValue = (bool?)e.NewValue; + base.OnPropertyChanged(change); - switch (newValue) + if (change.Property == IsCheckedProperty) { - case true: - OnChecked(new RoutedEventArgs(CheckedEvent)); - break; - case false: - OnUnchecked(new RoutedEventArgs(UncheckedEvent)); - break; - default: - OnIndeterminate(new RoutedEventArgs(IndeterminateEvent)); - break; + var newValue = change.GetNewValue(); + +#pragma warning disable CS0618 // Type or member is obsolete + switch (newValue) + { + case true: + OnChecked(new RoutedEventArgs(CheckedEvent)); + break; + case false: + OnUnchecked(new RoutedEventArgs(UncheckedEvent)); + break; + default: + OnIndeterminate(new RoutedEventArgs(IndeterminateEvent)); + break; + } +#pragma warning restore CS0618 // Type or member is obsolete + + OnIsCheckedChanged(new RoutedEventArgs(IsCheckedChangedEvent)); } } diff --git a/src/Avalonia.Controls/SplitButton/SplitButton.cs b/src/Avalonia.Controls/SplitButton/SplitButton.cs index 1d2bab8d27..31a06d875a 100644 --- a/src/Avalonia.Controls/SplitButton/SplitButton.cs +++ b/src/Avalonia.Controls/SplitButton/SplitButton.cs @@ -25,7 +25,7 @@ namespace Avalonia.Controls /// /// Raised when the user presses the primary part of the . /// - public event EventHandler Click + public event EventHandler? Click { add => AddHandler(ClickEvent, value); remove => RemoveHandler(ClickEvent, value); @@ -35,7 +35,9 @@ namespace Avalonia.Controls /// Defines the event. /// public static readonly RoutedEvent ClickEvent = - RoutedEvent.Register(nameof(Click), RoutingStrategies.Bubble); + RoutedEvent.Register( + nameof(Click), + RoutingStrategies.Bubble); /// /// Defines the property. @@ -69,10 +71,6 @@ namespace Avalonia.Controls private IDisposable? _flyoutPropertyChangedDisposable; - //////////////////////////////////////////////////////////////////////// - // Constructor / Destructors - //////////////////////////////////////////////////////////////////////// - /// /// Initializes a new instance of the class. /// @@ -80,10 +78,6 @@ namespace Avalonia.Controls { } - //////////////////////////////////////////////////////////////////////// - // Properties - //////////////////////////////////////////////////////////////////////// - /// /// Gets or sets the invoked when the primary part is pressed. /// @@ -124,10 +118,6 @@ namespace Avalonia.Controls /// protected override bool IsEnabledCore => base.IsEnabledCore && _commandCanExecute; - //////////////////////////////////////////////////////////////////////// - // Methods - //////////////////////////////////////////////////////////////////////// - /// void ICommandSource.CanExecuteChanged(object sender, EventArgs e) => this.CanExecuteChanged(sender, e); @@ -222,10 +212,6 @@ namespace Avalonia.Controls } } - //////////////////////////////////////////////////////////////////////// - // OnEvent Overridable Methods - //////////////////////////////////////////////////////////////////////// - /// protected override void OnApplyTemplate(TemplateAppliedEventArgs e) { @@ -429,10 +415,6 @@ namespace Avalonia.Controls // Available for derived types } - //////////////////////////////////////////////////////////////////////// - // Event Handling - //////////////////////////////////////////////////////////////////////// - /// /// Event handler for when the internal primary button part is pressed. /// diff --git a/src/Avalonia.Controls/SplitButton/ToggleSplitButton.cs b/src/Avalonia.Controls/SplitButton/ToggleSplitButton.cs index 83bc9851c1..70d2983b9b 100644 --- a/src/Avalonia.Controls/SplitButton/ToggleSplitButton.cs +++ b/src/Avalonia.Controls/SplitButton/ToggleSplitButton.cs @@ -18,7 +18,7 @@ namespace Avalonia.Controls /// /// Raised when the property value changes. /// - public event EventHandler IsCheckedChanged + public event EventHandler? IsCheckedChanged { add => AddHandler(IsCheckedChangedEvent, value); remove => RemoveHandler(IsCheckedChangedEvent, value); @@ -39,10 +39,6 @@ namespace Avalonia.Controls AvaloniaProperty.Register( nameof(IsChecked)); - //////////////////////////////////////////////////////////////////////// - // Constructor / Destructors - //////////////////////////////////////////////////////////////////////// - /// /// Initializes a new instance of the class. /// @@ -50,10 +46,6 @@ namespace Avalonia.Controls { } - //////////////////////////////////////////////////////////////////////// - // Properties - //////////////////////////////////////////////////////////////////////// - /// /// Gets or sets a value indicating whether the is checked. /// @@ -73,10 +65,6 @@ namespace Avalonia.Controls /// Type IStyleable.StyleKey => typeof(SplitButton); - //////////////////////////////////////////////////////////////////////// - // Methods - //////////////////////////////////////////////////////////////////////// - /// /// Toggles the property between true and false. /// @@ -85,10 +73,6 @@ namespace Avalonia.Controls IsChecked = !IsChecked; } - //////////////////////////////////////////////////////////////////////// - // OnEvent Overridable Methods - //////////////////////////////////////////////////////////////////////// - /// protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs e) { diff --git a/src/Avalonia.Controls/TextBlock.cs b/src/Avalonia.Controls/TextBlock.cs index 7a0da1ecc5..9bd1dc95f9 100644 --- a/src/Avalonia.Controls/TextBlock.cs +++ b/src/Avalonia.Controls/TextBlock.cs @@ -173,13 +173,7 @@ namespace Avalonia.Controls /// /// Gets the used to render the text. /// - public TextLayout TextLayout - { - get - { - return _textLayout ??= CreateTextLayout(_text); - } - } + public TextLayout TextLayout => _textLayout ??= CreateTextLayout(_text); /// /// Gets or sets the padding to place around the . @@ -648,6 +642,7 @@ namespace Avalonia.Controls /// protected void InvalidateTextLayout() { + _textLayout?.Dispose(); _textLayout = null; InvalidateVisual(); @@ -663,6 +658,7 @@ namespace Avalonia.Controls _constraint = availableSize.Deflate(padding); + _textLayout?.Dispose(); _textLayout = null; var inlines = Inlines; @@ -726,6 +722,7 @@ namespace Avalonia.Controls _constraint = new Size(Math.Ceiling(finalSize.Deflate(padding).Width), double.PositiveInfinity); + _textLayout?.Dispose(); _textLayout = null; if (HasComplexContent) @@ -829,12 +826,12 @@ namespace Avalonia.Controls protected readonly record struct SimpleTextSource : ITextSource { - private readonly CharacterBufferRange _text; + private readonly string _text; private readonly TextRunProperties _defaultProperties; public SimpleTextSource(string text, TextRunProperties defaultProperties) { - _text = new CharacterBufferRange(new CharacterBufferReference(text), text.Length); + _text = text; _defaultProperties = defaultProperties; } @@ -845,14 +842,14 @@ namespace Avalonia.Controls return new TextEndOfParagraph(); } - var runText = _text.Skip(textSourceIndex); + var runText = _text.AsMemory(textSourceIndex); if (runText.IsEmpty) { return new TextEndOfParagraph(); } - return new TextCharacters(runText.CharacterBufferReference, runText.Length, _defaultProperties); + return new TextCharacters(runText, _defaultProperties); } } @@ -887,14 +884,9 @@ namespace Avalonia.Controls if (textRun is TextCharacters) { - var characterBufferReference = textRun.CharacterBufferReference; - var skip = Math.Max(0, textSourceIndex - currentPosition); - return new TextCharacters( - new CharacterBufferReference(characterBufferReference.CharacterBuffer, characterBufferReference.OffsetToFirstChar + skip), - textRun.Length - skip, - textRun.Properties!); + return new TextCharacters(textRun.Text.Slice(skip), textRun.Properties!); } return textRun; diff --git a/src/Avalonia.Controls/TextBox.cs b/src/Avalonia.Controls/TextBox.cs index f388dc871e..9d07fb024a 100644 --- a/src/Avalonia.Controls/TextBox.cs +++ b/src/Avalonia.Controls/TextBox.cs @@ -961,9 +961,7 @@ namespace Avalonia.Controls var length = 0; - var inputRange = new CharacterBufferRange(new CharacterBufferReference(input), input.Length); - - var graphemeEnumerator = new GraphemeEnumerator(inputRange); + var graphemeEnumerator = new GraphemeEnumerator(input.AsSpan()); while (graphemeEnumerator.MoveNext()) { @@ -981,7 +979,7 @@ namespace Avalonia.Controls } } - length += grapheme.Length; + length += grapheme.Text.Length; } if (length < input.Length) diff --git a/src/Avalonia.Controls/TextBoxTextInputMethodClient.cs b/src/Avalonia.Controls/TextBoxTextInputMethodClient.cs index 52815b943d..c1146cceda 100644 --- a/src/Avalonia.Controls/TextBoxTextInputMethodClient.cs +++ b/src/Avalonia.Controls/TextBoxTextInputMethodClient.cs @@ -79,12 +79,10 @@ namespace Avalonia.Controls { if(run.Length > 0) { - var characterBufferRange = new CharacterBufferRange(run.CharacterBufferReference, run.Length); - #if NET6_0 - builder.Append(characterBufferRange.Span); + builder.Append(run.Text.Span); #else - builder.Append(characterBufferRange.Span.ToArray()); + builder.Append(run.Text.Span.ToArray()); #endif } } diff --git a/src/Avalonia.Controls/TopLevel.cs b/src/Avalonia.Controls/TopLevel.cs index 1f5b8f80d2..c0265e28b9 100644 --- a/src/Avalonia.Controls/TopLevel.cs +++ b/src/Avalonia.Controls/TopLevel.cs @@ -7,6 +7,7 @@ using Avalonia.Controls.Primitives; using Avalonia.Input; using Avalonia.Input.Raw; using Avalonia.Input.TextInput; +using Avalonia.Interactivity; using Avalonia.Layout; using Avalonia.Logging; using Avalonia.LogicalTree; @@ -17,6 +18,8 @@ using Avalonia.Rendering; using Avalonia.Styling; using Avalonia.Utilities; using Avalonia.VisualTree; +using Avalonia.Input.Platform; +using System.Linq; namespace Avalonia.Controls { @@ -76,6 +79,12 @@ namespace Avalonia.Controls public static readonly StyledProperty TransparencyBackgroundFallbackProperty = AvaloniaProperty.Register(nameof(TransparencyBackgroundFallback), Brushes.White); + /// + /// Defines the event. + /// + public static readonly RoutedEvent BackRequestedEvent = + RoutedEvent.Register(nameof(BackRequested), RoutingStrategies.Bubble); + private static readonly WeakEvent ResourcesChangedWeakEvent = WeakEvent.Register( (s, h) => s.ResourcesChanged += h, @@ -89,6 +98,7 @@ namespace Avalonia.Controls private readonly IGlobalStyles? _globalStyles; private readonly PointerOverPreProcessor? _pointerOverPreProcessor; private readonly IDisposable? _pointerOverPreProcessorSubscription; + private readonly IDisposable? _backGestureSubscription; private Size _clientSize; private Size? _frameSize; private WindowTransparencyLevel _actualTransparencyLevel; @@ -205,6 +215,48 @@ namespace Avalonia.Controls _pointerOverPreProcessor = new PointerOverPreProcessor(this); _pointerOverPreProcessorSubscription = _inputManager?.PreProcess.Subscribe(_pointerOverPreProcessor); + + if(impl is ITopLevelWithSystemNavigationManager topLevelWithSystemNavigation) + { + topLevelWithSystemNavigation.SystemNavigationManager.BackRequested += (s, e) => + { + e.RoutedEvent = BackRequestedEvent; + RaiseEvent(e); + }; + } + + _backGestureSubscription = _inputManager?.PreProcess.Subscribe(e => + { + bool backRequested = false; + + if (e is RawKeyEventArgs rawKeyEventArgs && rawKeyEventArgs.Type == RawKeyEventType.KeyDown) + { + var keymap = AvaloniaLocator.Current.GetService()?.Back; + + if (keymap != null) + { + var keyEvent = new KeyEventArgs() + { + KeyModifiers = (KeyModifiers)rawKeyEventArgs.Modifiers, + Key = rawKeyEventArgs.Key + }; + + backRequested = keymap.Any( key => key.Matches(keyEvent)); + } + } + else if(e is RawPointerEventArgs pointerEventArgs) + { + backRequested = pointerEventArgs.Type == RawPointerEventType.XButton1Down; + } + + if (backRequested) + { + var backRequestedEventArgs = new RoutedEventArgs(BackRequestedEvent); + RaiseEvent(backRequestedEventArgs); + + e.Handled = backRequestedEventArgs.Handled; + } + }); } /// @@ -263,6 +315,15 @@ namespace Avalonia.Controls set => SetValue(TransparencyBackgroundFallbackProperty, value); } + /// + /// Occurs when physical Back Button is pressed or a back navigation has been requested. + /// + public event EventHandler BackRequested + { + add { AddHandler(BackRequestedEvent, value); } + remove { RemoveHandler(BackRequestedEvent, value); } + } + public ILayoutManager LayoutManager { get @@ -341,6 +402,16 @@ namespace Avalonia.Controls { return PlatformImpl?.PointToScreen(p) ?? default; } + + /// + /// Gets the for which the given is hosted in. + /// + /// The visual to query its TopLevel + /// The TopLevel + public static TopLevel? GetTopLevel(Visual? visual) + { + return visual == null ? null : visual.VisualRoot as TopLevel; + } /// /// Creates the layout manager for this . @@ -372,6 +443,7 @@ namespace Avalonia.Controls _pointerOverPreProcessor?.OnCompleted(); _pointerOverPreProcessorSubscription?.Dispose(); + _backGestureSubscription?.Dispose(); PlatformImpl = null; diff --git a/src/Avalonia.DesignerSupport/Remote/Stubs.cs b/src/Avalonia.DesignerSupport/Remote/Stubs.cs index 3238b394fc..72d03c4b7f 100644 --- a/src/Avalonia.DesignerSupport/Remote/Stubs.cs +++ b/src/Avalonia.DesignerSupport/Remote/Stubs.cs @@ -182,7 +182,9 @@ namespace Avalonia.DesignerSupport.Remote public bool IsClientAreaExtendedToDecorations { get; } public bool NeedsManagedDecorations => false; - + + public void SetFrameThemeVariant(PlatformThemeVariant themeVariant) { } + public AcrylicPlatformCompensationLevels AcrylicCompensationLevels { get; } = new AcrylicPlatformCompensationLevels(1, 1, 1); } diff --git a/src/Avalonia.FreeDesktop/DBusMenuExporter.cs b/src/Avalonia.FreeDesktop/DBusMenuExporter.cs index 560d6fc252..0e6bee5f24 100644 --- a/src/Avalonia.FreeDesktop/DBusMenuExporter.cs +++ b/src/Avalonia.FreeDesktop/DBusMenuExporter.cs @@ -25,9 +25,9 @@ namespace Avalonia.FreeDesktop return new DBusMenuExporterImpl(DBusHelper.Connection, xid); } - public static INativeMenuExporter TryCreateDetachedNativeMenu(ObjectPath path, Connection currentConection) + public static INativeMenuExporter TryCreateDetachedNativeMenu(ObjectPath path, Connection currentConnection) { - return new DBusMenuExporterImpl(currentConection, path); + return new DBusMenuExporterImpl(currentConnection, path); } public static ObjectPath GenerateDBusMenuObjPath => "/net/avaloniaui/dbusmenu/" diff --git a/src/Avalonia.FreeDesktop/DBusPlatformSettings.cs b/src/Avalonia.FreeDesktop/DBusPlatformSettings.cs new file mode 100644 index 0000000000..039fc7c088 --- /dev/null +++ b/src/Avalonia.FreeDesktop/DBusPlatformSettings.cs @@ -0,0 +1,77 @@ +using System; +using System.Threading.Tasks; +using Avalonia.Logging; +using Avalonia.Platform; + +namespace Avalonia.FreeDesktop; + +internal class DBusPlatformSettings : DefaultPlatformSettings +{ + private readonly IDBusSettings? _settings; + private PlatformColorValues? _lastColorValues; + + public DBusPlatformSettings() + { + _settings = DBusHelper.TryInitialize()? + .CreateProxy("org.freedesktop.portal.Desktop", "/org/freedesktop/portal/desktop"); + + if (_settings is not null) + { + _ = _settings.WatchSettingChangedAsync(SettingsChangedHandler); + + _ = TryGetInitialValue(); + } + } + + public override PlatformColorValues GetColorValues() + { + return _lastColorValues ?? base.GetColorValues(); + } + + private async Task TryGetInitialValue() + { + var colorSchemeTask = _settings!.ReadAsync("org.freedesktop.appearance", "color-scheme"); + if (colorSchemeTask.Status == TaskStatus.RanToCompletion) + { + _lastColorValues = GetColorValuesFromSetting(colorSchemeTask.Result); + } + else + { + try + { + var value = await colorSchemeTask; + _lastColorValues = GetColorValuesFromSetting(value); + OnColorValuesChanged(_lastColorValues); + } + catch (Exception ex) + { + _lastColorValues = base.GetColorValues(); + Logger.TryGet(LogEventLevel.Error, LogArea.FreeDesktopPlatform)?.Log(this, "Unable to get setting value", ex); + } + } + } + + private void SettingsChangedHandler((string @namespace, string key, object value) tuple) + { + if (tuple.@namespace == "org.freedesktop.appearance" + && tuple.key == "color-scheme") + { + /* + 0: No preference + 1: Prefer dark appearance + 2: Prefer light appearance + */ + _lastColorValues = GetColorValuesFromSetting(tuple.value); + OnColorValuesChanged(_lastColorValues); + } + } + + private static PlatformColorValues GetColorValuesFromSetting(object value) + { + var isDark = value?.ToString() == "1"; + return new PlatformColorValues + { + ThemeVariant = isDark ? PlatformThemeVariant.Dark : PlatformThemeVariant.Light + }; + } +} diff --git a/src/Avalonia.FreeDesktop/DBusSettings.cs b/src/Avalonia.FreeDesktop/DBusSettings.cs new file mode 100644 index 0000000000..05911981c7 --- /dev/null +++ b/src/Avalonia.FreeDesktop/DBusSettings.cs @@ -0,0 +1,16 @@ +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using Tmds.DBus; + +namespace Avalonia.FreeDesktop; + +[DBusInterface("org.freedesktop.portal.Settings")] +internal interface IDBusSettings : IDBusObject +{ + Task<(string @namespace, IDictionary)> ReadAllAsync(string[] namespaces); + + Task ReadAsync(string @namespace, string key); + + Task WatchSettingChangedAsync(Action<(string @namespace, string key, object value)> handler, Action? onError = null); +} diff --git a/src/Avalonia.FreeDesktop/LinuxMountedVolumeInfoListener.cs b/src/Avalonia.FreeDesktop/LinuxMountedVolumeInfoListener.cs index 9f6b056d30..53a6f4519e 100644 --- a/src/Avalonia.FreeDesktop/LinuxMountedVolumeInfoListener.cs +++ b/src/Avalonia.FreeDesktop/LinuxMountedVolumeInfoListener.cs @@ -1,14 +1,10 @@ using System; using System.IO; using System.Linq; -using System.Collections.Generic; using System.Collections.ObjectModel; using Avalonia.Controls.Platform; using Avalonia.Reactive; -using Avalonia.Reactive; using System.Text.RegularExpressions; -using System.Runtime.InteropServices; -using System.Text; using Avalonia.Threading; namespace Avalonia.FreeDesktop diff --git a/src/Avalonia.Headless/HeadlessPlatformRenderInterface.cs b/src/Avalonia.Headless/HeadlessPlatformRenderInterface.cs index 9bb4b34976..cb79cc85db 100644 --- a/src/Avalonia.Headless/HeadlessPlatformRenderInterface.cs +++ b/src/Avalonia.Headless/HeadlessPlatformRenderInterface.cs @@ -128,6 +128,11 @@ namespace Avalonia.Headless public void Dispose() { } + + public IReadOnlyList GetIntersections(float lowerBound, float upperBound) + { + throw new NotImplementedException(); + } } class HeadlessGeometryStub : IGeometryImpl diff --git a/src/Avalonia.Headless/HeadlessPlatformStubs.cs b/src/Avalonia.Headless/HeadlessPlatformStubs.cs index 1cc0fa73bb..2a04e624cb 100644 --- a/src/Avalonia.Headless/HeadlessPlatformStubs.cs +++ b/src/Avalonia.Headless/HeadlessPlatformStubs.cs @@ -145,15 +145,13 @@ namespace Avalonia.Headless class HeadlessTextShaperStub : ITextShaperImpl { - public ShapedBuffer ShapeText(CharacterBufferReference text, int length, TextShaperOptions options) + public ShapedBuffer ShapeText(ReadOnlyMemory text, TextShaperOptions options) { var typeface = options.Typeface; var fontRenderingEmSize = options.FontRenderingEmSize; var bidiLevel = options.BidiLevel; - var characterBufferRange = new CharacterBufferRange(text, length); - - return new ShapedBuffer(characterBufferRange, length, typeface, fontRenderingEmSize, bidiLevel); + return new ShapedBuffer(text, text.Length, typeface, fontRenderingEmSize, bidiLevel); } } diff --git a/src/Avalonia.Headless/HeadlessWindowImpl.cs b/src/Avalonia.Headless/HeadlessWindowImpl.cs index 8eafce208b..195328cd65 100644 --- a/src/Avalonia.Headless/HeadlessWindowImpl.cs +++ b/src/Avalonia.Headless/HeadlessWindowImpl.cs @@ -345,5 +345,10 @@ namespace Avalonia.Headless { } + + public void SetFrameThemeVariant(PlatformThemeVariant themeVariant) + { + + } } } diff --git a/src/Avalonia.Native/AvaloniaNativeDragSource.cs b/src/Avalonia.Native/AvaloniaNativeDragSource.cs index bec45c2d71..7f4c462ee0 100644 --- a/src/Avalonia.Native/AvaloniaNativeDragSource.cs +++ b/src/Avalonia.Native/AvaloniaNativeDragSource.cs @@ -6,7 +6,6 @@ using Avalonia.Input; using Avalonia.Input.Platform; using Avalonia.Interactivity; using Avalonia.Native.Interop; -using Avalonia.VisualTree; namespace Avalonia.Native { @@ -26,7 +25,7 @@ namespace Avalonia.Native if (element == null) return null; var visual = (Visual)element; - return visual.GetVisualRoot() as TopLevel; + return TopLevel.GetTopLevel(visual); } class DndCallback : NativeCallbackBase, IAvnDndResultCallback diff --git a/src/Avalonia.Native/AvaloniaNativePlatform.cs b/src/Avalonia.Native/AvaloniaNativePlatform.cs index 6d5925c0ae..fc23fbb226 100644 --- a/src/Avalonia.Native/AvaloniaNativePlatform.cs +++ b/src/Avalonia.Native/AvaloniaNativePlatform.cs @@ -104,7 +104,7 @@ namespace Avalonia.Native .Bind().ToConstant(new CursorFactory(_factory.CreateCursorFactory())) .Bind().ToSingleton() .Bind().ToConstant(KeyboardDevice) - .Bind().ToSingleton() + .Bind().ToConstant(new NativePlatformSettings(_factory.CreatePlatformSettings())) .Bind().ToConstant(this) .Bind().ToConstant(new ClipboardImpl(_factory.CreateClipboard())) .Bind().ToConstant(new DefaultRenderTimer(60)) diff --git a/src/Avalonia.Native/MacOSMountedVolumeInfoProvider.cs b/src/Avalonia.Native/MacOSMountedVolumeInfoProvider.cs index 6f45baeec2..e79c7a40d3 100644 --- a/src/Avalonia.Native/MacOSMountedVolumeInfoProvider.cs +++ b/src/Avalonia.Native/MacOSMountedVolumeInfoProvider.cs @@ -3,7 +3,6 @@ using System.Collections.ObjectModel; using System.IO; using System.Linq; using Avalonia.Reactive; -using Avalonia.Reactive; using Avalonia.Controls.Platform; using Avalonia.Threading; diff --git a/src/Avalonia.Native/NativePlatformSettings.cs b/src/Avalonia.Native/NativePlatformSettings.cs new file mode 100644 index 0000000000..53d3da0378 --- /dev/null +++ b/src/Avalonia.Native/NativePlatformSettings.cs @@ -0,0 +1,77 @@ +using System; +using Avalonia.Media; +using Avalonia.Native.Interop; +using Avalonia.Platform; + +namespace Avalonia.Native; + +internal class NativePlatformSettings : DefaultPlatformSettings +{ + private readonly IAvnPlatformSettings _platformSettings; + private PlatformColorValues _lastColorValues; + + public NativePlatformSettings(IAvnPlatformSettings platformSettings) + { + _platformSettings = platformSettings; + platformSettings.RegisterColorsChange(new ColorsChangeCallback(this)); + } + + public override PlatformColorValues GetColorValues() + { + var (theme, contrast) = _platformSettings.PlatformTheme switch + { + AvnPlatformThemeVariant.Dark => (PlatformThemeVariant.Dark, ColorContrastPreference.NoPreference), + AvnPlatformThemeVariant.Light => (PlatformThemeVariant.Light, ColorContrastPreference.NoPreference), + AvnPlatformThemeVariant.HighContrastDark => (PlatformThemeVariant.Dark, ColorContrastPreference.High), + AvnPlatformThemeVariant.HighContrastLight => (PlatformThemeVariant.Dark, ColorContrastPreference.High), + _ => throw new ArgumentOutOfRangeException() + }; + var color = _platformSettings.AccentColor; + + if (color > 0) + { + _lastColorValues = new PlatformColorValues + { + ThemeVariant = theme, + ContrastPreference = contrast, + AccentColor1 = Color.FromUInt32(color) + }; + } + else + { + _lastColorValues = new PlatformColorValues + { + ThemeVariant = theme, + ContrastPreference = contrast + }; + } + + return _lastColorValues; + } + + public void OnColorValuesChanged() + { + var oldColorValues = _lastColorValues; + var colorValues = GetColorValues(); + + if (oldColorValues != colorValues) + { + OnColorValuesChanged(colorValues); + } + } + + private class ColorsChangeCallback : NativeCallbackBase, IAvnActionCallback + { + private readonly NativePlatformSettings _settings; + + public ColorsChangeCallback(NativePlatformSettings settings) + { + _settings = settings; + } + + public void Run() + { + _settings.OnColorValuesChanged(); + } + } +} diff --git a/src/Avalonia.Native/WindowImplBase.cs b/src/Avalonia.Native/WindowImplBase.cs index ca57e30b1c..7686b42275 100644 --- a/src/Avalonia.Native/WindowImplBase.cs +++ b/src/Avalonia.Native/WindowImplBase.cs @@ -519,6 +519,11 @@ namespace Avalonia.Native public WindowTransparencyLevel TransparencyLevel { get; private set; } = WindowTransparencyLevel.Transparent; + public void SetFrameThemeVariant(PlatformThemeVariant themeVariant) + { + _native.SetFrameThemeVariant((AvnPlatformThemeVariant)themeVariant); + } + public AcrylicPlatformCompensationLevels AcrylicCompensationLevels { get; } = new AcrylicPlatformCompensationLevels(1, 0, 0); public IPlatformHandle Handle { get; private set; } diff --git a/src/Avalonia.Native/avn.idl b/src/Avalonia.Native/avn.idl index 8057b162f5..a062fdc61d 100644 --- a/src/Avalonia.Native/avn.idl +++ b/src/Avalonia.Native/avn.idl @@ -473,6 +473,14 @@ enum AvnWindowTransparencyMode Blur } +enum AvnPlatformThemeVariant +{ + Light, + Dark, + HighContrastLight, + HighContrastDark, +} + [uuid(809c652e-7396-11d2-9771-00a0c9b4d50c)] interface IAvaloniaNativeFactory : IUnknown { @@ -494,6 +502,7 @@ interface IAvaloniaNativeFactory : IUnknown HRESULT CreateMenuItemSeparator(IAvnMenuItem** ppv); HRESULT CreateTrayIcon(IAvnTrayIcon** ppv); HRESULT CreateApplicationCommands(IAvnApplicationCommands** ppv); + HRESULT CreatePlatformSettings(IAvnPlatformSettings** ppv); } [uuid(233e094f-9b9f-44a3-9a6e-6948bbdd9fb1)] @@ -535,6 +544,7 @@ interface IAvnWindowBase : IUnknown HRESULT BeginDragAndDropOperation(AvnDragDropEffects effects, AvnPoint point, IAvnClipboard* clipboard, IAvnDndResultCallback* cb, [intptr]void* sourceHandle); HRESULT SetTransparencyMode(AvnWindowTransparencyMode mode); + HRESULT SetFrameThemeVariant(AvnPlatformThemeVariant mode); } [uuid(83e588f3-6981-4e48-9ea0-e1e569f79a91), cpp-virtual-inherits] @@ -906,3 +916,11 @@ interface IAvnAutomationNode : IUnknown void PropertyChanged(AvnAutomationProperty property); void FocusChanged(); } + +[uuid(d1f009cc-9d2d-493b-845d-90d2c104baae)] +interface IAvnPlatformSettings : IUnknown +{ + AvnPlatformThemeVariant GetPlatformTheme(); + uint GetAccentColor(); + void RegisterColorsChange(IAvnActionCallback* callback); +} diff --git a/src/Avalonia.X11/X11Platform.cs b/src/Avalonia.X11/X11Platform.cs index 0f9ef72a3f..4cc2cca39d 100644 --- a/src/Avalonia.X11/X11Platform.cs +++ b/src/Avalonia.X11/X11Platform.cs @@ -80,7 +80,7 @@ namespace Avalonia.X11 .Bind().ToFunc(() => KeyboardDevice) .Bind().ToConstant(new X11CursorFactory(Display)) .Bind().ToConstant(new X11Clipboard(this)) - .Bind().ToSingleton() + .Bind().ToSingleton() .Bind().ToConstant(new X11IconLoader()) .Bind().ToConstant(new LinuxMountedVolumeInfoProvider()) .Bind().ToConstant(new X11PlatformLifetimeEvents(this)); diff --git a/src/Avalonia.X11/X11Window.cs b/src/Avalonia.X11/X11Window.cs index 5bbcf3af8c..861fed0803 100644 --- a/src/Avalonia.X11/X11Window.cs +++ b/src/Avalonia.X11/X11Window.cs @@ -1226,6 +1226,8 @@ namespace Avalonia.X11 public WindowTransparencyLevel TransparencyLevel => _transparencyHelper?.CurrentLevel ?? WindowTransparencyLevel.None; + public void SetFrameThemeVariant(PlatformThemeVariant themeVariant) { } + public AcrylicPlatformCompensationLevels AcrylicCompensationLevels { get; } = new AcrylicPlatformCompensationLevels(1, 0.8, 0.8); public bool NeedsManagedDecorations => false; diff --git a/src/Browser/Avalonia.Browser/BrowserPlatformSettings.cs b/src/Browser/Avalonia.Browser/BrowserPlatformSettings.cs new file mode 100644 index 0000000000..6084c5c7de --- /dev/null +++ b/src/Browser/Avalonia.Browser/BrowserPlatformSettings.cs @@ -0,0 +1,31 @@ +using Avalonia.Browser.Interop; +using Avalonia.Platform; + +namespace Avalonia.Browser; + +internal class BrowserPlatformSettings : DefaultPlatformSettings +{ + private bool _isDarkMode; + private bool _isHighContrast; + + public BrowserPlatformSettings() + { + var obj = DomHelper.ObserveDarkMode((isDarkMode, isHighContrast) => + { + _isDarkMode = isDarkMode; + _isHighContrast = isHighContrast; + OnColorValuesChanged(GetColorValues()); + }); + _isDarkMode = obj.GetPropertyAsBoolean("isDarkMode"); + _isHighContrast = obj.GetPropertyAsBoolean("isHighContrast"); + } + + public override PlatformColorValues GetColorValues() + { + return base.GetColorValues() with + { + ThemeVariant = _isDarkMode ? PlatformThemeVariant.Dark : PlatformThemeVariant.Light, + ContrastPreference = _isHighContrast ? ColorContrastPreference.High : ColorContrastPreference.NoPreference + }; + } +} diff --git a/src/Browser/Avalonia.Browser/BrowserSystemNavigationManager.cs b/src/Browser/Avalonia.Browser/BrowserSystemNavigationManager.cs new file mode 100644 index 0000000000..cc7d8c3af1 --- /dev/null +++ b/src/Browser/Avalonia.Browser/BrowserSystemNavigationManager.cs @@ -0,0 +1,24 @@ +using System; +using Avalonia.Browser.Interop; +using Avalonia.Interactivity; +using Avalonia.Platform; + +namespace Avalonia.Browser +{ + internal class BrowserSystemNavigationManager : ISystemNavigationManager + { + public event EventHandler? BackRequested; + + public BrowserSystemNavigationManager() + { + NavigationHelper.AddBackHandler(() => + { + var routedEventArgs = new RoutedEventArgs(); + + BackRequested?.Invoke(this, routedEventArgs); + + return routedEventArgs.Handled; + }); + } + } +} diff --git a/src/Browser/Avalonia.Browser/BrowserTopLevelImpl.cs b/src/Browser/Avalonia.Browser/BrowserTopLevelImpl.cs index 4e6be3268b..3affcfb4c2 100644 --- a/src/Browser/Avalonia.Browser/BrowserTopLevelImpl.cs +++ b/src/Browser/Avalonia.Browser/BrowserTopLevelImpl.cs @@ -19,7 +19,8 @@ using Avalonia.Rendering.Composition; namespace Avalonia.Browser { - internal class BrowserTopLevelImpl : ITopLevelImplWithTextInputMethod, ITopLevelImplWithNativeControlHost, ITopLevelImplWithStorageProvider + internal class BrowserTopLevelImpl : ITopLevelImplWithTextInputMethod, ITopLevelImplWithNativeControlHost, ITopLevelImplWithStorageProvider, + ITopLevelWithSystemNavigationManager { private Size _clientSize; private IInputRoot? _inputRoot; @@ -228,11 +229,18 @@ namespace Avalonia.Browser public IKeyboardDevice KeyboardDevice { get; } = BrowserWindowingPlatform.Keyboard; public WindowTransparencyLevel TransparencyLevel { get; private set; } + public void SetFrameThemeVariant(PlatformThemeVariant themeVariant) + { + // not in the standard, but we potentially can use "apple-mobile-web-app-status-bar-style" for iOS and "theme-color" for android. + } + public AcrylicPlatformCompensationLevels AcrylicCompensationLevels { get; } public ITextInputMethodImpl TextInputMethod => _avaloniaView; public INativeControlHostImpl? NativeControlHost { get; } public IStorageProvider StorageProvider { get; } = new BrowserStorageProvider(); + + public ISystemNavigationManager SystemNavigationManager { get; } = new BrowserSystemNavigationManager(); } } diff --git a/src/Browser/Avalonia.Browser/Interop/DomHelper.cs b/src/Browser/Avalonia.Browser/Interop/DomHelper.cs index b97a03209b..d1133b8916 100644 --- a/src/Browser/Avalonia.Browser/Interop/DomHelper.cs +++ b/src/Browser/Avalonia.Browser/Interop/DomHelper.cs @@ -25,4 +25,9 @@ internal static partial class DomHelper public static partial double ObserveDpi( [JSMarshalAs>] Action onDpiChanged); + + [JSImport("AvaloniaDOM.observeDarkMode", AvaloniaModule.MainModuleName)] + public static partial JSObject ObserveDarkMode( + [JSMarshalAs>] + Action observer); } diff --git a/src/Browser/Avalonia.Browser/Interop/NavigationHelper.cs b/src/Browser/Avalonia.Browser/Interop/NavigationHelper.cs new file mode 100644 index 0000000000..d49b83b5a0 --- /dev/null +++ b/src/Browser/Avalonia.Browser/Interop/NavigationHelper.cs @@ -0,0 +1,10 @@ +using System; +using System.Runtime.InteropServices.JavaScript; + +namespace Avalonia.Browser.Interop; + +internal static partial class NavigationHelper +{ + [JSImport("NavigationHelper.addBackHandler", AvaloniaModule.MainModuleName)] + public static partial void AddBackHandler([JSMarshalAs>] Func backHandlerCallback); +} diff --git a/src/Browser/Avalonia.Browser/Storage/BrowserStorageProvider.cs b/src/Browser/Avalonia.Browser/Storage/BrowserStorageProvider.cs index dc580f0fcb..0bebcb9f63 100644 --- a/src/Browser/Avalonia.Browser/Storage/BrowserStorageProvider.cs +++ b/src/Browser/Avalonia.Browser/Storage/BrowserStorageProvider.cs @@ -29,11 +29,11 @@ internal class BrowserStorageProvider : IStorageProvider await _lazyModule.Value; var startIn = (options.SuggestedStartLocation as JSStorageItem)?.FileHandle; - var (types, exludeAll) = ConvertFileTypes(options.FileTypeFilter); + var (types, excludeAll) = ConvertFileTypes(options.FileTypeFilter); try { - using var items = await StorageHelper.OpenFileDialog(startIn, options.AllowMultiple, types, exludeAll); + using var items = await StorageHelper.OpenFileDialog(startIn, options.AllowMultiple, types, excludeAll); if (items is null) { return Array.Empty(); @@ -63,11 +63,11 @@ internal class BrowserStorageProvider : IStorageProvider await _lazyModule.Value; var startIn = (options.SuggestedStartLocation as JSStorageItem)?.FileHandle; - var (types, exludeAll) = ConvertFileTypes(options.FileTypeChoices); + var (types, excludeAll) = ConvertFileTypes(options.FileTypeChoices); try { - var item = await StorageHelper.SaveFileDialog(startIn, options.SuggestedFileName, types, exludeAll); + var item = await StorageHelper.SaveFileDialog(startIn, options.SuggestedFileName, types, excludeAll); return item is not null ? new JSStorageFile(item) : null; } catch (JSException ex) when (ex.Message.Contains(PickerCancelMessage, StringComparison.Ordinal)) @@ -127,9 +127,9 @@ internal class BrowserStorageProvider : IStorageProvider types = null; } - var inlcudeAll = input?.Contains(FilePickerFileTypes.All) == true || types is null; + var includeAll = input?.Contains(FilePickerFileTypes.All) == true || types is null; - return (types, !inlcudeAll); + return (types, !includeAll); } } diff --git a/src/Browser/Avalonia.Browser/WindowingPlatform.cs b/src/Browser/Avalonia.Browser/WindowingPlatform.cs index 6493374a50..46db8936eb 100644 --- a/src/Browser/Avalonia.Browser/WindowingPlatform.cs +++ b/src/Browser/Avalonia.Browser/WindowingPlatform.cs @@ -39,7 +39,7 @@ namespace Avalonia.Browser .Bind().ToSingleton() .Bind().ToSingleton() .Bind().ToConstant(s_keyboard) - .Bind().ToSingleton() + .Bind().ToSingleton() .Bind().ToConstant(instance) .Bind().ToConstant(new RenderLoop()) .Bind().ToConstant(ManualTriggerRenderTimer.Instance) diff --git a/src/Browser/Avalonia.Browser/webapp/modules/avalonia.ts b/src/Browser/Avalonia.Browser/webapp/modules/avalonia.ts index 0642bd475d..ab0c85eaa2 100644 --- a/src/Browser/Avalonia.Browser/webapp/modules/avalonia.ts +++ b/src/Browser/Avalonia.Browser/webapp/modules/avalonia.ts @@ -5,6 +5,7 @@ import { AvaloniaDOM } from "./avalonia/dom"; import { Caniuse } from "./avalonia/caniuse"; import { StreamHelper } from "./avalonia/stream"; import { NativeControlHost } from "./avalonia/nativeControlHost"; +import { NavigationHelper } from "./avalonia/navigationHelper"; async function registerAvaloniaModule(api: RuntimeAPI): Promise { api.setModuleImports("avalonia", { @@ -15,7 +16,8 @@ async function registerAvaloniaModule(api: RuntimeAPI): Promise { DpiWatcher, AvaloniaDOM, StreamHelper, - NativeControlHost + NativeControlHost, + NavigationHelper }); } export { @@ -27,6 +29,7 @@ export { AvaloniaDOM, StreamHelper, NativeControlHost, + NavigationHelper, registerAvaloniaModule }; diff --git a/src/Browser/Avalonia.Browser/webapp/modules/avalonia/dom.ts b/src/Browser/Avalonia.Browser/webapp/modules/avalonia/dom.ts index e8e37faf58..b99f8e7907 100644 --- a/src/Browser/Avalonia.Browser/webapp/modules/avalonia/dom.ts +++ b/src/Browser/Avalonia.Browser/webapp/modules/avalonia/dom.ts @@ -3,6 +3,27 @@ export class AvaloniaDOM { element.classList.add(className); } + static observeDarkMode(observer: (isDarkMode: boolean, isHighContrast: boolean) => boolean) { + if (globalThis.matchMedia === undefined) { + return false; + } + + const colorShemeMedia = globalThis.matchMedia("(prefers-color-scheme: dark)"); + const prefersContrastMedia = globalThis.matchMedia("(prefers-contrast: more)"); + + colorShemeMedia.addEventListener("change", (args: MediaQueryListEvent) => { + observer(args.matches, prefersContrastMedia.matches); + }); + prefersContrastMedia.addEventListener("change", (args: MediaQueryListEvent) => { + observer(colorShemeMedia.matches, args.matches); + }); + + return { + isDarkMode: colorShemeMedia.matches, + isHighContrast: prefersContrastMedia.matches + }; + } + static createAvaloniaHost(host: HTMLElement) { const randomIdPart = Math.random().toString(36).replace(/[^a-z]+/g, "").substr(2, 10); diff --git a/src/Browser/Avalonia.Browser/webapp/modules/avalonia/navigationHelper.ts b/src/Browser/Avalonia.Browser/webapp/modules/avalonia/navigationHelper.ts new file mode 100644 index 0000000000..ee594fe85c --- /dev/null +++ b/src/Browser/Avalonia.Browser/webapp/modules/avalonia/navigationHelper.ts @@ -0,0 +1,14 @@ +export class NavigationHelper { + public static addBackHandler(backHandlerCallback: () => Boolean) { + history.pushState(null, "", window.location.href); + window.onpopstate = () => { + const handled = backHandlerCallback(); + + if (!handled) { + history.back(); + } else { + history.forward(); + } + }; + } +} diff --git a/src/Linux/Avalonia.LinuxFramebuffer/FramebufferToplevelImpl.cs b/src/Linux/Avalonia.LinuxFramebuffer/FramebufferToplevelImpl.cs index 6032f3a92c..cb158097eb 100644 --- a/src/Linux/Avalonia.LinuxFramebuffer/FramebufferToplevelImpl.cs +++ b/src/Linux/Avalonia.LinuxFramebuffer/FramebufferToplevelImpl.cs @@ -83,6 +83,8 @@ namespace Avalonia.LinuxFramebuffer public WindowTransparencyLevel TransparencyLevel { get; private set; } + public void SetFrameThemeVariant(PlatformThemeVariant themeVariant) { } + public AcrylicPlatformCompensationLevels AcrylicCompensationLevels { get; } = new AcrylicPlatformCompensationLevels(1, 1, 1); } } diff --git a/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlSelectorTransformer.cs b/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlSelectorTransformer.cs index ba0da4fbe0..078e23bc02 100644 --- a/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlSelectorTransformer.cs +++ b/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlSelectorTransformer.cs @@ -124,7 +124,7 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions.Transformers $"Cannot convert '{attachedProperty.Value}' to '{targetPropertyType.GetFqn()}", node); - result = new XamlIlAttacchedPropertyEqualsSelector(result, targetPropertyField, typedValue); + result = new XamlIlAttachedPropertyEqualsSelector(result, targetPropertyField, typedValue); break; } case SelectorGrammar.ChildSyntax child: @@ -414,9 +414,9 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions.Transformers } - class XamlIlAttacchedPropertyEqualsSelector : XamlIlSelectorNode + class XamlIlAttachedPropertyEqualsSelector : XamlIlSelectorNode { - public XamlIlAttacchedPropertyEqualsSelector(XamlIlSelectorNode previous, + public XamlIlAttachedPropertyEqualsSelector(XamlIlSelectorNode previous, IXamlField propertyFiled, IXamlAstValueNode value) : base(previous) diff --git a/src/Skia/Avalonia.Skia/GlyphRunImpl.cs b/src/Skia/Avalonia.Skia/GlyphRunImpl.cs index 484fd9f219..dd7ed31a6e 100644 --- a/src/Skia/Avalonia.Skia/GlyphRunImpl.cs +++ b/src/Skia/Avalonia.Skia/GlyphRunImpl.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Generic; using Avalonia.Metadata; using Avalonia.Platform; using SkiaSharp; @@ -20,6 +21,9 @@ namespace Avalonia.Skia /// public SKTextBlob TextBlob { get; } + public IReadOnlyList GetIntersections(float upperBound, float lowerBound) => + TextBlob.GetIntercepts(lowerBound, upperBound); + void IDisposable.Dispose() { TextBlob.Dispose(); diff --git a/src/Skia/Avalonia.Skia/TextShaperImpl.cs b/src/Skia/Avalonia.Skia/TextShaperImpl.cs index 98eb35d5c5..e0f95bac60 100644 --- a/src/Skia/Avalonia.Skia/TextShaperImpl.cs +++ b/src/Skia/Avalonia.Skia/TextShaperImpl.cs @@ -1,5 +1,7 @@ using System; +using System.Buffers; using System.Globalization; +using System.Runtime.InteropServices; using Avalonia.Media.TextFormatting; using Avalonia.Media.TextFormatting.Unicode; using Avalonia.Platform; @@ -11,9 +13,9 @@ namespace Avalonia.Skia { internal class TextShaperImpl : ITextShaperImpl { - public ShapedBuffer ShapeText(CharacterBufferReference characterBufferReference, int length, TextShaperOptions options) + public ShapedBuffer ShapeText(ReadOnlyMemory text, TextShaperOptions options) { - var text = new CharacterBufferRange(characterBufferReference, length); + var textSpan = text.Span; var typeface = options.Typeface; var fontRenderingEmSize = options.FontRenderingEmSize; var bidiLevel = options.BidiLevel; @@ -21,7 +23,9 @@ namespace Avalonia.Skia using (var buffer = new Buffer()) { - buffer.AddUtf16(characterBufferReference.CharacterBuffer.Span, characterBufferReference.OffsetToFirstChar, length); + // HarfBuzz needs the surrounding characters to correctly shape the text + var containingText = GetContainingMemory(text, out var start, out var length); + buffer.AddUtf16(containingText.Span, start, length); MergeBreakPair(buffer); @@ -64,7 +68,7 @@ namespace Avalonia.Skia var glyphOffset = GetGlyphOffset(glyphPositions, i, textScale); - if (text[i] == '\t') + if (textSpan[i] == '\t') { glyphIndex = typeface.GetGlyph(' '); @@ -147,5 +151,26 @@ namespace Avalonia.Skia // glyphPositions[index].YAdvance * textScale; return glyphPositions[index].XAdvance * textScale; } + + private static ReadOnlyMemory GetContainingMemory(ReadOnlyMemory memory, out int start, out int length) + { + if (MemoryMarshal.TryGetString(memory, out var containingString, out start, out length)) + { + return containingString.AsMemory(); + } + + if (MemoryMarshal.TryGetArray(memory, out var segment)) + { + return segment.Array.AsMemory(); + } + + if (MemoryMarshal.TryGetMemoryManager(memory, out MemoryManager memoryManager, out start, out length)) + { + return memoryManager.Memory; + } + + // should never happen + throw new InvalidOperationException("Memory not backed by string, array or manager"); + } } } diff --git a/src/Windows/Avalonia.Direct2D1/Media/GlyphRunImpl.cs b/src/Windows/Avalonia.Direct2D1/Media/GlyphRunImpl.cs index 0b06d5ef3e..67418613a4 100644 --- a/src/Windows/Avalonia.Direct2D1/Media/GlyphRunImpl.cs +++ b/src/Windows/Avalonia.Direct2D1/Media/GlyphRunImpl.cs @@ -1,4 +1,5 @@ -using Avalonia.Platform; +using System.Collections.Generic; +using Avalonia.Platform; namespace Avalonia.Direct2D1.Media { @@ -13,7 +14,13 @@ namespace Avalonia.Direct2D1.Media public void Dispose() { - GlyphRun?.Dispose(); + //SharpDX already handles this. + //GlyphRun?.Dispose(); + } + + public IReadOnlyList GetIntersections(float lowerBound, float upperBound) + { + return null; } } } diff --git a/src/Windows/Avalonia.Direct2D1/Media/TextShaperImpl.cs b/src/Windows/Avalonia.Direct2D1/Media/TextShaperImpl.cs index 6685dd00b9..fffa5ce490 100644 --- a/src/Windows/Avalonia.Direct2D1/Media/TextShaperImpl.cs +++ b/src/Windows/Avalonia.Direct2D1/Media/TextShaperImpl.cs @@ -1,5 +1,7 @@ using System; +using System.Buffers; using System.Globalization; +using System.Runtime.InteropServices; using Avalonia.Media.TextFormatting; using Avalonia.Media.TextFormatting.Unicode; using Avalonia.Platform; @@ -11,8 +13,9 @@ namespace Avalonia.Direct2D1.Media { internal class TextShaperImpl : ITextShaperImpl { - public ShapedBuffer ShapeText(CharacterBufferReference characterBufferReference, int length, TextShaperOptions options) + public ShapedBuffer ShapeText(ReadOnlyMemory text, TextShaperOptions options) { + var textSpan = text.Span; var typeface = options.Typeface; var fontRenderingEmSize = options.FontRenderingEmSize; var bidiLevel = options.BidiLevel; @@ -20,7 +23,9 @@ namespace Avalonia.Direct2D1.Media using (var buffer = new Buffer()) { - buffer.AddUtf16(characterBufferReference.CharacterBuffer.Span, characterBufferReference.OffsetToFirstChar, length); + // HarfBuzz needs the surrounding characters to correctly shape the text + var containingText = GetContainingMemory(text, out var start, out var length); + buffer.AddUtf16(containingText.Span, start, length); MergeBreakPair(buffer); @@ -34,7 +39,7 @@ namespace Avalonia.Direct2D1.Media font.Shape(buffer); - if(buffer.Direction == Direction.RightToLeft) + if (buffer.Direction == Direction.RightToLeft) { buffer.Reverse(); } @@ -45,9 +50,7 @@ namespace Avalonia.Direct2D1.Media var bufferLength = buffer.Length; - var characterBufferRange = new CharacterBufferRange(characterBufferReference, length); - - var shapedBuffer = new ShapedBuffer(characterBufferRange, bufferLength, typeface, fontRenderingEmSize, bidiLevel); + var shapedBuffer = new ShapedBuffer(text, bufferLength, typeface, fontRenderingEmSize, bidiLevel); var glyphInfos = buffer.GetGlyphInfoSpan(); @@ -61,11 +64,11 @@ namespace Avalonia.Direct2D1.Media var glyphCluster = (int)(sourceInfo.Cluster); - var glyphAdvance = GetGlyphAdvance(glyphPositions, i, textScale); + var glyphAdvance = GetGlyphAdvance(glyphPositions, i, textScale) + options.LetterSpacing; var glyphOffset = GetGlyphOffset(glyphPositions, i, textScale); - if (characterBufferRange[i] == '\t') + if (textSpan[i] == '\t') { glyphIndex = typeface.GetGlyph(' '); @@ -148,5 +151,26 @@ namespace Avalonia.Direct2D1.Media // glyphPositions[index].YAdvance * textScale; return glyphPositions[index].XAdvance * textScale; } + + private static ReadOnlyMemory GetContainingMemory(ReadOnlyMemory memory, out int start, out int length) + { + if (MemoryMarshal.TryGetString(memory, out var containingString, out start, out length)) + { + return containingString.AsMemory(); + } + + if (MemoryMarshal.TryGetArray(memory, out var segment)) + { + return segment.Array.AsMemory(); + } + + if (MemoryMarshal.TryGetMemoryManager(memory, out MemoryManager memoryManager, out start, out length)) + { + return memoryManager.Memory; + } + + // should never happen + throw new InvalidOperationException("Memory not backed by string, array or manager"); + } } } diff --git a/src/Windows/Avalonia.Win32.Interop/Wpf/WpfTopLevelImpl.cs b/src/Windows/Avalonia.Win32.Interop/Wpf/WpfTopLevelImpl.cs index 16a20c855f..76fc4fa21d 100644 --- a/src/Windows/Avalonia.Win32.Interop/Wpf/WpfTopLevelImpl.cs +++ b/src/Windows/Avalonia.Win32.Interop/Wpf/WpfTopLevelImpl.cs @@ -258,6 +258,8 @@ namespace Avalonia.Win32.Interop.Wpf public WindowTransparencyLevel TransparencyLevel { get; private set; } + public void SetFrameThemeVariant(PlatformThemeVariant themeVariant) { } + public AcrylicPlatformCompensationLevels AcrylicCompensationLevels { get; } = new AcrylicPlatformCompensationLevels(1, 1, 1); } } diff --git a/src/Windows/Avalonia.Win32/Input/WindowsMouseDevice.cs b/src/Windows/Avalonia.Win32/Input/WindowsMouseDevice.cs index cf1bdc1671..998ff4a427 100644 --- a/src/Windows/Avalonia.Win32/Input/WindowsMouseDevice.cs +++ b/src/Windows/Avalonia.Win32/Input/WindowsMouseDevice.cs @@ -1,7 +1,6 @@ using System; using Avalonia.Controls; using Avalonia.Input; -using Avalonia.VisualTree; using Avalonia.Win32.Interop; namespace Avalonia.Win32.Input @@ -34,7 +33,7 @@ namespace Avalonia.Win32.Input protected override void PlatformCapture(IInputElement element) { - var hwnd = (((element as Visual)?.GetVisualRoot() as TopLevel)?.PlatformImpl as WindowImpl) + var hwnd = (TopLevel.GetTopLevel(element as Visual)?.PlatformImpl as WindowImpl) ?.Handle.Handle; if (hwnd.HasValue && hwnd != IntPtr.Zero) diff --git a/src/Windows/Avalonia.Win32/Win32Platform.cs b/src/Windows/Avalonia.Win32/Win32Platform.cs index e3d0cd9e5f..7e1e00bded 100644 --- a/src/Windows/Avalonia.Win32/Win32Platform.cs +++ b/src/Windows/Avalonia.Win32/Win32Platform.cs @@ -11,6 +11,7 @@ using Avalonia.Controls.ApplicationLifetimes; using Avalonia.Controls.Platform; using Avalonia.Input; using Avalonia.Input.Platform; +using Avalonia.Media; using Avalonia.OpenGL; using Avalonia.Platform; using Avalonia.Rendering; @@ -19,6 +20,7 @@ using Avalonia.Threading; using Avalonia.Utilities; using Avalonia.Win32.Input; using Avalonia.Win32.Interop; +using Avalonia.Win32.WinRT; using static Avalonia.Win32.Interop.UnmanagedMethods; namespace Avalonia @@ -111,7 +113,7 @@ namespace Avalonia namespace Avalonia.Win32 { - public class Win32Platform : IPlatformThreadingInterface, IPlatformSettings, IWindowingPlatform, IPlatformIconLoader, IPlatformLifetimeEventsImpl + public class Win32Platform : IPlatformThreadingInterface, IWindowingPlatform, IPlatformIconLoader, IPlatformLifetimeEventsImpl { private static readonly Win32Platform s_instance = new Win32Platform(); private static Thread _uiThread; @@ -126,6 +128,7 @@ namespace Avalonia.Win32 } internal static Win32Platform Instance => s_instance; + internal static IPlatformSettings PlatformSettings => AvaloniaLocator.Current.GetRequiredService(); internal IntPtr Handle => _hwnd; @@ -153,7 +156,7 @@ namespace Avalonia.Win32 .Bind().ToSingleton() .Bind().ToConstant(CursorFactory.Instance) .Bind().ToConstant(WindowsKeyboardDevice.Instance) - .Bind().ToConstant(s_instance) + .Bind().ToSingleton() .Bind().ToConstant(s_instance) .Bind().ToConstant(new RenderLoop()) .Bind().ToConstant(new DefaultRenderTimer(60)) @@ -258,8 +261,6 @@ namespace Avalonia.Win32 public bool CurrentThreadIsLoopThread => _uiThread == Thread.CurrentThread; - public TimeSpan HoldWaitDuration { get; set; } = TimeSpan.FromMilliseconds(300); - public event Action Signaled; public event EventHandler ShutdownRequested; @@ -286,6 +287,17 @@ namespace Avalonia.Win32 } } } + + if (msg == (uint)WindowsMessage.WM_SETTINGCHANGE + && PlatformSettings is Win32PlatformSettings win32PlatformSettings) + { + var changedSetting = Marshal.PtrToStringAuto(lParam); + if (changedSetting == "ImmersiveColorSet" // dark/light mode + || changedSetting == "WindowsThemeElement") // high contrast mode + { + win32PlatformSettings.OnColorValuesChanged(); + } + } TrayIconImpl.ProcWnd(hWnd, msg, wParam, lParam); @@ -403,25 +415,5 @@ namespace Avalonia.Win32 SetProcessDPIAware(); } - - Size IPlatformSettings.GetTapSize(PointerType type) - { - return type switch - { - PointerType.Touch => new(10, 10), - _ => new(GetSystemMetrics(SystemMetric.SM_CXDRAG), GetSystemMetrics(SystemMetric.SM_CYDRAG)), - }; - } - - Size IPlatformSettings.GetDoubleTapSize(PointerType type) - { - return type switch - { - PointerType.Touch => new(16, 16), - _ => new(GetSystemMetrics(SystemMetric.SM_CXDOUBLECLK), GetSystemMetrics(SystemMetric.SM_CYDOUBLECLK)), - }; - } - - TimeSpan IPlatformSettings.GetDoubleTapTime(PointerType type) => TimeSpan.FromMilliseconds(GetDoubleClickTime()); } } diff --git a/src/Windows/Avalonia.Win32/Win32PlatformSettings.cs b/src/Windows/Avalonia.Win32/Win32PlatformSettings.cs new file mode 100644 index 0000000000..0310c4bfad --- /dev/null +++ b/src/Windows/Avalonia.Win32/Win32PlatformSettings.cs @@ -0,0 +1,89 @@ +using System; +using System.Runtime.InteropServices; +using Avalonia.Input; +using Avalonia.Platform; +using Avalonia.Win32.Interop; +using Avalonia.Win32.WinRT; +using static Avalonia.Win32.Interop.UnmanagedMethods; + +namespace Avalonia.Win32; + +internal class Win32PlatformSettings : DefaultPlatformSettings +{ + private PlatformColorValues _lastColorValues; + + public override Size GetTapSize(PointerType type) + { + return type switch + { + PointerType.Touch => new(10, 10), + _ => new(GetSystemMetrics(UnmanagedMethods.SystemMetric.SM_CXDRAG), GetSystemMetrics(UnmanagedMethods.SystemMetric.SM_CYDRAG)), + }; + } + + public override Size GetDoubleTapSize(PointerType type) + { + return type switch + { + PointerType.Touch => new(16, 16), + _ => new(GetSystemMetrics(UnmanagedMethods.SystemMetric.SM_CXDOUBLECLK), GetSystemMetrics(UnmanagedMethods.SystemMetric.SM_CYDOUBLECLK)), + }; + } + + public override TimeSpan GetDoubleTapTime(PointerType type) => TimeSpan.FromMilliseconds(GetDoubleClickTime()); + + public override PlatformColorValues GetColorValues() + { + if (Win32Platform.WindowsVersion.Major < 10) + { + return base.GetColorValues(); + } + + var uiSettings = NativeWinRTMethods.CreateInstance("Windows.UI.ViewManagement.UISettings"); + var accent = uiSettings.GetColorValue(UIColorType.Accent).ToAvalonia(); + + var accessibilitySettings = NativeWinRTMethods.CreateInstance("Windows.UI.ViewManagement.AccessibilitySettings"); + if (accessibilitySettings.HighContrast == 1) + { + // Windows 11 has 4 different high contrast schemes: + // - Aquatic - High Contrast Black + // - Desert - High Contrast White + // - Dusk - High Contrast #1 + // - Night sky - High Contrast #2 + // Only "Desert" one can be considered a "light" preference. + using var highContrastScheme = new HStringInterop(accessibilitySettings.HighContrastScheme); + return _lastColorValues = new PlatformColorValues + { + ThemeVariant = highContrastScheme.Value?.Contains("White") == true ? + PlatformThemeVariant.Light : + PlatformThemeVariant.Dark, + ContrastPreference = ColorContrastPreference.High, + // Windows provides more than one accent color for the HighContrast themes, but with no API for that (at least not in the WinRT) + AccentColor1 = accent + }; + } + else + { + var background = uiSettings.GetColorValue(UIColorType.Background).ToAvalonia(); + return _lastColorValues = new PlatformColorValues + { + ThemeVariant = background.R + background.G + background.B < (255 * 3 - background.R - background.G - background.B) ? + PlatformThemeVariant.Dark : + PlatformThemeVariant.Light, + ContrastPreference = ColorContrastPreference.NoPreference, + AccentColor1 = accent + }; + } + } + + internal void OnColorValuesChanged() + { + var oldColorValues = _lastColorValues; + var colorValues = GetColorValues(); + + if (oldColorValues != colorValues) + { + OnColorValuesChanged(colorValues); + } + } +} diff --git a/src/Windows/Avalonia.Win32/WinRT/NativeWinRTMethods.cs b/src/Windows/Avalonia.Win32/WinRT/NativeWinRTMethods.cs index 1794b022fe..01b45f3df2 100644 --- a/src/Windows/Avalonia.Win32/WinRT/NativeWinRTMethods.cs +++ b/src/Windows/Avalonia.Win32/WinRT/NativeWinRTMethods.cs @@ -22,6 +22,9 @@ namespace Avalonia.Win32.WinRT internal static IntPtr WindowsCreateString(string sourceString) => WindowsCreateString(sourceString, sourceString.Length); + [DllImport("api-ms-win-core-winrt-string-l1-1-0.dll", CallingConvention = CallingConvention.StdCall)] + internal static extern unsafe char* WindowsGetStringRawBuffer(IntPtr hstring, uint* length); + [DllImport("api-ms-win-core-winrt-string-l1-1-0.dll", CallingConvention = CallingConvention.StdCall, PreserveSig = false)] internal static extern unsafe void WindowsDeleteString(IntPtr hString); @@ -120,17 +123,38 @@ namespace Avalonia.Win32.WinRT class HStringInterop : IDisposable { private IntPtr _s; + private bool _owns; public HStringInterop(string s) { _s = s == null ? IntPtr.Zero : NativeWinRTMethods.WindowsCreateString(s); + _owns = true; + } + + public HStringInterop(IntPtr str, bool owns = false) + { + _s = str; + _owns = owns; } public IntPtr Handle => _s; + public unsafe string Value + { + get + { + if (_s == IntPtr.Zero) + return null; + + uint length; + var buffer = NativeWinRTMethods.WindowsGetStringRawBuffer(_s, &length); + return new string(buffer, 0, (int) length); + } + } + public void Dispose() { - if (_s != IntPtr.Zero) + if (_s != IntPtr.Zero && _owns) { NativeWinRTMethods.WindowsDeleteString(_s); _s = IntPtr.Zero; diff --git a/src/Windows/Avalonia.Win32/WinRT/WinRTColor.cs b/src/Windows/Avalonia.Win32/WinRT/WinRTColor.cs index 35f0737d5f..90b2022054 100644 --- a/src/Windows/Avalonia.Win32/WinRT/WinRTColor.cs +++ b/src/Windows/Avalonia.Win32/WinRT/WinRTColor.cs @@ -1,4 +1,5 @@ using System.Runtime.InteropServices; +using Avalonia.Media; namespace Avalonia.Win32.WinRT { @@ -14,5 +15,7 @@ namespace Avalonia.Win32.WinRT { A = a, R = r, G = g, B = b }; + + public Color ToAvalonia() => new(A, R, G, B); } } diff --git a/src/Windows/Avalonia.Win32/WinRT/winrt.idl b/src/Windows/Avalonia.Win32/WinRT/winrt.idl index ffb98fafc8..beb88352dd 100644 --- a/src/Windows/Avalonia.Win32/WinRT/winrt.idl +++ b/src/Windows/Avalonia.Win32/WinRT/winrt.idl @@ -844,3 +844,30 @@ interface ICompositor6 : IInspectable [overload("CreateGeometricClip")] HRESULT CreateGeometricClip([out] [retval] ICompositionGeometricClip** result); [overload("CreateGeometricClip")] HRESULT CreateGeometricClipWithGeometry([in] ICompositionGeometry* geometry, [out] [retval] ICompositionGeometricClip** result); } + +enum UIColorType +{ + Background = 0, + Foreground = 1, + AccentDark3 = 2, + AccentDark2 = 3, + AccentDark1 = 4, + Accent = 5, + AccentLight1 = 6, + AccentLight2 = 7, + AccentLight3 = 8, + Complement = 9 +} + +[uuid(03021BE4-5254-4781-8194-5168F7D06D7B)] +interface IUISettings3 : IInspectable +{ + HRESULT GetColorValue([in] UIColorType desiredColor, [out][retval] Color* value); +} + +[uuid(FE0E8147-C4C0-4562-B962-1327B52AD5B9)] +interface IAccessibilitySettings : IInspectable +{ + [propget] HRESULT HighContrast([out] [retval] boolean* value); + [propget] HRESULT HighContrastScheme([out] [retval] HSTRING* value); +} diff --git a/src/Windows/Avalonia.Win32/WindowImpl.cs b/src/Windows/Avalonia.Win32/WindowImpl.cs index dbcc31c5f5..a7741477e5 100644 --- a/src/Windows/Avalonia.Win32/WindowImpl.cs +++ b/src/Windows/Avalonia.Win32/WindowImpl.cs @@ -805,6 +805,19 @@ namespace Avalonia.Win32 _topmost = value; } + public unsafe void SetFrameThemeVariant(PlatformThemeVariant themeVariant) + { + if (Win32Platform.WindowsVersion.Build >= 22000) + { + var pvUseBackdropBrush = themeVariant == PlatformThemeVariant.Dark ? 1 : 0; + DwmSetWindowAttribute( + _hwnd, + (int)DwmWindowAttribute.DWMWA_USE_IMMERSIVE_DARK_MODE, + &pvUseBackdropBrush, + sizeof(int)); + } + } + protected virtual IntPtr CreateWindowOverride(ushort atom) { return CreateWindowEx( diff --git a/src/Windows/Avalonia.Win32/WindowsMountedVolumeInfoListener.cs b/src/Windows/Avalonia.Win32/WindowsMountedVolumeInfoListener.cs index 94e9c0c814..3244e75a34 100644 --- a/src/Windows/Avalonia.Win32/WindowsMountedVolumeInfoListener.cs +++ b/src/Windows/Avalonia.Win32/WindowsMountedVolumeInfoListener.cs @@ -3,7 +3,6 @@ using System.Collections.ObjectModel; using System.IO; using System.Linq; using Avalonia.Reactive; -using Avalonia.Reactive; using Avalonia.Controls.Platform; using Avalonia.Logging; using Avalonia.Threading; diff --git a/src/iOS/Avalonia.iOS/AvaloniaView.cs b/src/iOS/Avalonia.iOS/AvaloniaView.cs index 48358c745f..7442861597 100644 --- a/src/iOS/Avalonia.iOS/AvaloniaView.cs +++ b/src/iOS/Avalonia.iOS/AvaloniaView.cs @@ -52,6 +52,22 @@ namespace Avalonia.iOS public override bool CanResignFirstResponder => true; + public override void TraitCollectionDidChange(UITraitCollection previousTraitCollection) + { + base.TraitCollectionDidChange(previousTraitCollection); + + var settings = AvaloniaLocator.Current.GetRequiredService() as PlatformSettings; + settings?.TraitCollectionDidChange(); + } + + public override void TintColorDidChange() + { + base.TintColorDidChange(); + + var settings = AvaloniaLocator.Current.GetRequiredService() as PlatformSettings; + settings?.TraitCollectionDidChange(); + } + internal class TopLevelImpl : ITopLevelImplWithTextInputMethod, ITopLevelImplWithNativeControlHost, ITopLevelImplWithStorageProvider { @@ -120,7 +136,24 @@ namespace Avalonia.iOS // legacy no-op public IMouseDevice MouseDevice { get; } = new MouseDevice(); public WindowTransparencyLevel TransparencyLevel { get; } - + + public void SetFrameThemeVariant(PlatformThemeVariant themeVariant) + { + // TODO adjust status bar depending on full screen mode. + if (OperatingSystem.IsIOSVersionAtLeast(13)) + { + var uiStatusBarStyle = themeVariant switch + { + PlatformThemeVariant.Light => UIStatusBarStyle.DarkContent, + PlatformThemeVariant.Dark => UIStatusBarStyle.LightContent, + _ => throw new ArgumentOutOfRangeException(nameof(themeVariant), themeVariant, null) + }; + + // Consider using UIViewController.PreferredStatusBarStyle in the future. + UIApplication.SharedApplication.SetStatusBarStyle(uiStatusBarStyle, true); + } + } + public AcrylicPlatformCompensationLevels AcrylicCompensationLevels { get; } = new AcrylicPlatformCompensationLevels(); diff --git a/src/iOS/Avalonia.iOS/Platform.cs b/src/iOS/Avalonia.iOS/Platform.cs index 63025f7f0a..5bcd1eae84 100644 --- a/src/iOS/Avalonia.iOS/Platform.cs +++ b/src/iOS/Avalonia.iOS/Platform.cs @@ -40,7 +40,7 @@ namespace Avalonia.iOS .Bind().ToConstant(new CursorFactoryStub()) .Bind().ToConstant(new WindowingPlatformStub()) .Bind().ToConstant(new ClipboardImpl()) - .Bind().ToSingleton() + .Bind().ToSingleton() .Bind().ToConstant(new PlatformIconLoaderStub()) .Bind().ToSingleton() .Bind().ToSingleton() diff --git a/src/iOS/Avalonia.iOS/PlatformSettings.cs b/src/iOS/Avalonia.iOS/PlatformSettings.cs new file mode 100644 index 0000000000..0ae290468b --- /dev/null +++ b/src/iOS/Avalonia.iOS/PlatformSettings.cs @@ -0,0 +1,65 @@ +using System; +using System.Linq; +using Avalonia.Media; +using Avalonia.Platform; +using Foundation; +using UIKit; + +namespace Avalonia.iOS; + +// TODO: ideally should be created per view/activity. +internal class PlatformSettings : DefaultPlatformSettings +{ + private PlatformColorValues _lastColorValues; + + public override PlatformColorValues GetColorValues() + { + var themeVariant = UITraitCollection.CurrentTraitCollection.UserInterfaceStyle == UIUserInterfaceStyle.Dark ? + PlatformThemeVariant.Dark : + PlatformThemeVariant.Light; + + + var contrastPreference = UITraitCollection.CurrentTraitCollection.AccessibilityContrast == UIAccessibilityContrast.High ? + ColorContrastPreference.High : + ColorContrastPreference.NoPreference; + + UIColor? tintColor = null; + if (OperatingSystem.IsIOSVersionAtLeast(14)) + { + tintColor = UIConfigurationColorTransformer.PreferredTint(UIColor.Clear); + } + + if (tintColor is not null) + { + tintColor.GetRGBA(out var red, out var green, out var blue, out var alpha); + return _lastColorValues = new PlatformColorValues + { + ThemeVariant = themeVariant, + ContrastPreference = contrastPreference, + AccentColor1 = new Color( + (byte)(alpha * 255), + (byte)(red * 255), + (byte)(green * 255), + (byte)(blue * 255)) + }; + } + else + { + return _lastColorValues = new PlatformColorValues + { + ThemeVariant = themeVariant, ContrastPreference = contrastPreference + }; + } + } + + public void TraitCollectionDidChange() + { + var oldColorValues = _lastColorValues; + var colorValues = GetColorValues(); + + if (oldColorValues != colorValues) + { + OnColorValuesChanged(colorValues); + } + } +} diff --git a/src/iOS/Avalonia.iOS/Storage/IOSStorageProvider.cs b/src/iOS/Avalonia.iOS/Storage/IOSStorageProvider.cs index 834418437b..5fdb7ecce1 100644 --- a/src/iOS/Avalonia.iOS/Storage/IOSStorageProvider.cs +++ b/src/iOS/Avalonia.iOS/Storage/IOSStorageProvider.cs @@ -34,7 +34,7 @@ internal class IOSStorageProvider : IStorageProvider UIDocumentPickerViewController documentPicker; if (OperatingSystem.IsIOSVersionAtLeast(14)) { - var allowedUtis = options.FileTypeFilter?.SelectMany(f => + var allowedUtils = options.FileTypeFilter?.SelectMany(f => { // We check for OS version outside of the lambda, it's safe. #pragma warning disable CA1416 @@ -56,18 +56,18 @@ internal class IOSStorageProvider : IStorageProvider UTTypes.Item, UTTypes.Data }; - documentPicker = new UIDocumentPickerViewController(allowedUtis!, false); + documentPicker = new UIDocumentPickerViewController(allowedUtils!, false); } else { - var allowedUtis = options.FileTypeFilter?.SelectMany(f => f.AppleUniformTypeIdentifiers ?? Array.Empty()) + var allowedUtils = options.FileTypeFilter?.SelectMany(f => f.AppleUniformTypeIdentifiers ?? Array.Empty()) .ToArray() ?? new[] { UTTypeLegacy.Content, UTTypeLegacy.Item, "public.data" }; - documentPicker = new UIDocumentPickerViewController(allowedUtis, UIDocumentPickerMode.Open); + documentPicker = new UIDocumentPickerViewController(allowedUtils, UIDocumentPickerMode.Open); } using (documentPicker) diff --git a/tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_SetValue.cs b/tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_SetValue.cs index f878977cf2..99c4ac3d86 100644 --- a/tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_SetValue.cs +++ b/tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_SetValue.cs @@ -227,7 +227,7 @@ namespace Avalonia.Base.UnitTests { Class2 target = new Class2(); - target.SetValue((AvaloniaProperty)Class2.FlobProperty, new ImplictDouble(4)); + target.SetValue((AvaloniaProperty)Class2.FlobProperty, new ImplicitDouble(4)); var value = target.GetValue(Class2.FlobProperty); Assert.IsType(value); @@ -393,16 +393,16 @@ namespace Avalonia.Base.UnitTests AvaloniaProperty.RegisterAttached("Attached"); } - private class ImplictDouble + private class ImplicitDouble { - public ImplictDouble(double value) + public ImplicitDouble(double value) { Value = value; } public double Value { get; } - public static implicit operator double (ImplictDouble v) + public static implicit operator double (ImplicitDouble v) { return v.Value; } diff --git a/tests/Avalonia.Base.UnitTests/Data/Core/ExpressionObserverTests_DataValidation.cs b/tests/Avalonia.Base.UnitTests/Data/Core/ExpressionObserverTests_DataValidation.cs index 43192584af..2a8bbaae79 100644 --- a/tests/Avalonia.Base.UnitTests/Data/Core/ExpressionObserverTests_DataValidation.cs +++ b/tests/Avalonia.Base.UnitTests/Data/Core/ExpressionObserverTests_DataValidation.cs @@ -49,7 +49,7 @@ namespace Avalonia.Base.UnitTests.Data.Core ////} [Fact] - public void Indei_Validation_Does_Not_Subscribe_When_DataValidatation_Not_Enabled() + public void Indei_Validation_Does_Not_Subscribe_When_DataValidation_Not_Enabled() { var data = new IndeiTest { MustBePositive = 5 }; var observer = ExpressionObserver.Create(data, o => o.MustBePositive, false); diff --git a/tests/Avalonia.Base.UnitTests/Data/Core/ExpressionObserverTests_ExpressionTree.cs b/tests/Avalonia.Base.UnitTests/Data/Core/ExpressionObserverTests_ExpressionTree.cs index 9b587d7679..6cd757331b 100644 --- a/tests/Avalonia.Base.UnitTests/Data/Core/ExpressionObserverTests_ExpressionTree.cs +++ b/tests/Avalonia.Base.UnitTests/Data/Core/ExpressionObserverTests_ExpressionTree.cs @@ -42,7 +42,7 @@ namespace Avalonia.Base.UnitTests.Data.Core } [Fact] - public void Property_Acccess_Expression_Can_Set_Property() + public void Property_Access_Expression_Can_Set_Property() { var data = new Class1(); var target = ExpressionObserver.Create(data, o => o.Foo); diff --git a/tests/Avalonia.Base.UnitTests/Input/GesturesTests.cs b/tests/Avalonia.Base.UnitTests/Input/GesturesTests.cs index 84ee35ba61..d466f6a4a5 100644 --- a/tests/Avalonia.Base.UnitTests/Input/GesturesTests.cs +++ b/tests/Avalonia.Base.UnitTests/Input/GesturesTests.cs @@ -506,7 +506,7 @@ namespace Avalonia.Base.UnitTests.Input } [Fact] - public void Scrolling_Should_Start_After_Start_Distance_Is_Exceded() + public void Scrolling_Should_Start_After_Start_Distance_Is_Exceeded() { Border border = new Border() { diff --git a/tests/Avalonia.Base.UnitTests/Interactivity/InteractiveTests.cs b/tests/Avalonia.Base.UnitTests/Interactivity/InteractiveTests.cs index 1a8b72d5e5..49119c3830 100644 --- a/tests/Avalonia.Base.UnitTests/Interactivity/InteractiveTests.cs +++ b/tests/Avalonia.Base.UnitTests/Interactivity/InteractiveTests.cs @@ -337,6 +337,27 @@ namespace Avalonia.Base.UnitTests.Interactivity Assert.True(target.GetVisualParent().ClassHandlerInvoked); } + [Fact] + public void Typed_Class_Handlers_Should_Be_Called_For_Handled_Events() + { + var ev = new RoutedEvent( + "test", + RoutingStrategies.Bubble | RoutingStrategies.Tunnel, + typeof(TestInteractive)); + + var target = CreateTree(ev, null, 0); + + ev.AddClassHandler((x, e) => x.MarkEventAsHandled(e), RoutingStrategies.Bubble); + ev.AddClassHandler((x, e) => x.ClassHandler(e), RoutingStrategies.Bubble, handledEventsToo: true); + + var args = new RoutedEventArgs(ev, target); + target.RaiseEvent(args); + + Assert.True(args.Handled); + Assert.True(target.ClassHandlerInvoked); + Assert.True(target.GetVisualParent().ClassHandlerInvoked); + } + [Fact] public void GetObservable_Should_Listen_To_Event() { @@ -443,6 +464,11 @@ namespace Avalonia.Base.UnitTests.Interactivity { ClassHandlerInvoked = true; } + + public void MarkEventAsHandled(RoutedEventArgs e) + { + e.Handled = true; + } } } } diff --git a/tests/Avalonia.Base.UnitTests/Media/TextFormatting/BiDiClassTests.cs b/tests/Avalonia.Base.UnitTests/Media/TextFormatting/BiDiClassTests.cs index b2c40f4ff1..9d189d1950 100644 --- a/tests/Avalonia.Base.UnitTests/Media/TextFormatting/BiDiClassTests.cs +++ b/tests/Avalonia.Base.UnitTests/Media/TextFormatting/BiDiClassTests.cs @@ -37,7 +37,7 @@ namespace Avalonia.Visuals.UnitTests.Media.TextFormatting var text = Encoding.UTF32.GetString(MemoryMarshal.Cast(t.CodePoints).ToArray()); // Append - bidiData.Append(new CharacterBufferRange(text)); + bidiData.Append(text); // Act for (int i = 0; i < 10; i++) diff --git a/tests/Avalonia.Base.UnitTests/Media/TextFormatting/GraphemeBreakClassTrieGeneratorTests.cs b/tests/Avalonia.Base.UnitTests/Media/TextFormatting/GraphemeBreakClassTrieGeneratorTests.cs index c57bd6c002..a022039000 100644 --- a/tests/Avalonia.Base.UnitTests/Media/TextFormatting/GraphemeBreakClassTrieGeneratorTests.cs +++ b/tests/Avalonia.Base.UnitTests/Media/TextFormatting/GraphemeBreakClassTrieGeneratorTests.cs @@ -38,18 +38,13 @@ namespace Avalonia.Base.UnitTests.Media.TextFormatting var text = Encoding.UTF32.GetString(MemoryMarshal.Cast(t.Codepoints).ToArray()); var grapheme = Encoding.UTF32.GetString(MemoryMarshal.Cast(t.Grapheme).ToArray()).AsSpan(); - var enumerator = new GraphemeEnumerator(new CharacterBufferRange(text)); + var enumerator = new GraphemeEnumerator(text); enumerator.MoveNext(); - var actual = text.AsSpan(enumerator.Current.Offset, enumerator.Current.Length); + var actual = enumerator.Current.Text; - var pass = true; - - if(actual.Length != grapheme.Length) - { - pass = false; - } + bool pass = actual.Length == grapheme.Length; if (pass) { @@ -87,13 +82,13 @@ namespace Avalonia.Base.UnitTests.Media.TextFormatting { const string text = "ABCDEFGHIJ"; - var enumerator = new GraphemeEnumerator(new CharacterBufferRange(text)); + var enumerator = new GraphemeEnumerator(text); var count = 0; while (enumerator.MoveNext()) { - Assert.Equal(1, enumerator.Current.Length); + Assert.Equal(1, enumerator.Current.Text.Length); count++; } diff --git a/tests/Avalonia.Base.UnitTests/Media/TextFormatting/LineBreakEnumuratorTests.cs b/tests/Avalonia.Base.UnitTests/Media/TextFormatting/LineBreakEnumuratorTests.cs index b2648bf348..d198fe81a6 100644 --- a/tests/Avalonia.Base.UnitTests/Media/TextFormatting/LineBreakEnumuratorTests.cs +++ b/tests/Avalonia.Base.UnitTests/Media/TextFormatting/LineBreakEnumuratorTests.cs @@ -23,7 +23,7 @@ namespace Avalonia.Base.UnitTests.Media.TextFormatting [Fact] public void BasicLatinTest() { - var lineBreaker = new LineBreakEnumerator(new CharacterBufferRange("Hello World\r\nThis is a test.")); + var lineBreaker = new LineBreakEnumerator("Hello World\r\nThis is a test."); Assert.True(lineBreaker.MoveNext()); Assert.Equal(6, lineBreaker.Current.PositionWrap); @@ -56,7 +56,7 @@ namespace Avalonia.Base.UnitTests.Media.TextFormatting [Fact] public void ForwardTextWithOuterWhitespace() { - var lineBreaker = new LineBreakEnumerator(new CharacterBufferRange(" Apples Pears Bananas ")); + var lineBreaker = new LineBreakEnumerator(" Apples Pears Bananas "); var positionsF = GetBreaks(lineBreaker); Assert.Equal(1, positionsF[0].PositionWrap); Assert.Equal(0, positionsF[0].PositionMeasure); @@ -83,7 +83,7 @@ namespace Avalonia.Base.UnitTests.Media.TextFormatting [Fact] public void ForwardTest() { - var lineBreaker = new LineBreakEnumerator(new CharacterBufferRange("Apples Pears Bananas")); + var lineBreaker = new LineBreakEnumerator("Apples Pears Bananas"); var positionsF = GetBreaks(lineBreaker); Assert.Equal(7, positionsF[0].PositionWrap); @@ -100,7 +100,7 @@ namespace Avalonia.Base.UnitTests.Media.TextFormatting { var text = string.Join(null, codePoints.Select(char.ConvertFromUtf32)); - var lineBreaker = new LineBreakEnumerator(new CharacterBufferRange(text)); + var lineBreaker = new LineBreakEnumerator(text); var foundBreaks = new List(); diff --git a/tests/Avalonia.Base.UnitTests/Rendering/CompositorTestsBase.cs b/tests/Avalonia.Base.UnitTests/Rendering/CompositorTestsBase.cs index 7f4e160000..12db249f04 100644 --- a/tests/Avalonia.Base.UnitTests/Rendering/CompositorTestsBase.cs +++ b/tests/Avalonia.Base.UnitTests/Rendering/CompositorTestsBase.cs @@ -104,6 +104,8 @@ public class CompositorTestsBase } public WindowTransparencyLevel TransparencyLevel { get; } + public void SetFrameThemeVariant(PlatformThemeVariant themeVariant) { } + public AcrylicPlatformCompensationLevels AcrylicCompensationLevels { get; } } diff --git a/tests/Avalonia.Base.UnitTests/VisualTests.cs b/tests/Avalonia.Base.UnitTests/VisualTests.cs index 973893af2d..fb214a6b34 100644 --- a/tests/Avalonia.Base.UnitTests/VisualTests.cs +++ b/tests/Avalonia.Base.UnitTests/VisualTests.cs @@ -126,7 +126,7 @@ namespace Avalonia.Base.UnitTests } [Fact] - public void Root_Should_Retun_Self_As_VisualRoot() + public void Root_Should_Return_Self_As_VisualRoot() { var root = new TestRoot(); @@ -134,7 +134,7 @@ namespace Avalonia.Base.UnitTests } [Fact] - public void Descendants_Should_RetunVisualRoot() + public void Descendants_Should_ReturnVisualRoot() { var root = new TestRoot(); var child1 = new Decorator(); diff --git a/tests/Avalonia.Benchmarks/NullGlyphRun.cs b/tests/Avalonia.Benchmarks/NullGlyphRun.cs index 5ba0822649..1c2c7c0d7d 100644 --- a/tests/Avalonia.Benchmarks/NullGlyphRun.cs +++ b/tests/Avalonia.Benchmarks/NullGlyphRun.cs @@ -1,4 +1,5 @@ -using Avalonia.Platform; +using System.Collections.Generic; +using Avalonia.Platform; namespace Avalonia.Benchmarks { @@ -7,5 +8,10 @@ namespace Avalonia.Benchmarks public void Dispose() { } + + public IReadOnlyList GetIntersections(float lowerBound, float upperBound) + { + return null; + } } } diff --git a/tests/Avalonia.Benchmarks/Text/HugeTextLayout.cs b/tests/Avalonia.Benchmarks/Text/HugeTextLayout.cs index e1b3c6a99c..bf0253e9ab 100644 --- a/tests/Avalonia.Benchmarks/Text/HugeTextLayout.cs +++ b/tests/Avalonia.Benchmarks/Text/HugeTextLayout.cs @@ -1,4 +1,6 @@ using System; +using System.Linq; +using Avalonia.Controls; using Avalonia.Media; using Avalonia.Media.TextFormatting; using Avalonia.UnitTests; @@ -10,9 +12,18 @@ namespace Avalonia.Benchmarks.Text; public class HugeTextLayout : IDisposable { private readonly IDisposable _app; + private string[] _manySmallStrings; + private static Random _rand = new Random(); + + private static string RandomString(int length) + { + const string chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789&?%$@"; + return new string(Enumerable.Repeat(chars, length).Select(s => s[_rand.Next(s.Length)]).ToArray()); + } public HugeTextLayout() { + _manySmallStrings = Enumerable.Range(0, 1000).Select(x => RandomString(_rand.Next(2, 15))).ToArray(); _app = UnitTestApplication.Start( TestServices.StyledWindow.With( renderInterface: new NullRenderingPlatform(), @@ -29,11 +40,13 @@ One should, however, not forget that concentration of violations of the strategi In a loose sense concentration of the center of the critical thinking provides a deep insight into the emergency planning. The comparison is quite a powerful matter. Resulting from review or analysis of threats and opportunities, we can presume that either significant improvement or basics of planning and scheduling reveals the patterns of the final draft. Therefore, the concept of the crucial component can be treated as the only solution. One should, nevertheless, consider that the exceptional results of the diverse sources of information gives an overview of the production cycle. It should rather be regarded as an integral part of the direct access to key resources. -Admitting that the possibility of achieving the results of the constructive criticism, as far as the strategic management is questionable, cannot rely only on the critical thinking. It may reveal how the systems approach partially the comprehensive project management. We must be ready for outline design stage and network development investigation of every contradiction between the effective time management and the efficient decision the network development. Everyone understands what it takes to the draft analysis and prior decisions and early design solutions. In any case, we can systematically change the mechanism of the sources and influences of the continuing financing doctrine. This could exceedingly be a result of a task analysis the hardware maintenance. The real reason of the strategic planning seemingly the influence on eventual productivity. Everyone understands what it takes to the well-known practice. Therefore, the concept of the productivity boost can be treated as the only solution the driving factor. It may reveal how the matters of peculiar interest slowly the goals and objectives or the diverse sources of information the positive influence of any major outcomes complete failure of the supposed theory. +Admitting that the possibility of achieving the results of the constructive criticism, as far as the strategic management is questionable, cannot rely only on the critical thinking. It may reveal how the systems approach partially the comprehensive project management. We must be ready for outline design stage and network development investigation of every contradiction between the effective time management and the efficient decision the network development. +Everyone understands what it takes to the draft analysis and prior decisions and early design solutions. In any case, we can systematically change the mechanism of the sources and influences of the continuing financing doctrine. This could exceedingly be a result of a task analysis the hardware maintenance. The real reason of the strategic planning seemingly the influence on eventual productivity. Everyone understands what it takes to the well-known practice. Therefore, the concept of the productivity boost can be treated as the only solution the driving factor. +It may reveal how the matters of peculiar interest slowly the goals and objectives or the diverse sources of information the positive influence of any major outcomes complete failure of the supposed theory. In respect that the structure of the sufficient amount poses problems and challenges for both the set of related commands and controls and the ability bias."; [Benchmark] - public TextLayout BuildTextLayout() => new TextLayout(Text, Typeface.Default, 12d, Brushes.Black); + public TextLayout BuildTextLayout() => MakeLayout(Text); private const string Emojis = @"😀 😁 😂 🤣 😃 😄 😅 😆 😉 😊 😋 😎 😍 😘 🥰 😗 😙 😚 ☺️ 🙂 🤗 🤩 🤔 🤨 😐 😑 😶 🙄 😏 😣 😥 😮 🤐 😯 😪 😫 😴 😌 😛 😜 😝 🤤 😒 😓 😔 😕 🙃 🤑 😲 ☹️ 🙁 😖 😞 😟 😤 😢 😭 😦 😧 😨 😩 🤯 😬 😰 😱 🥵 🥶 😳 🤪 😵 😡 😠 🤬 😷 🤒 🤕 🤢 🤮 🤧 😇 🤠 🤡 🥳 🥴 🥺 🤥 🤫 🤭 🧐 🤓 😈 👿 👹 👺 💀 👻 👽 🤖 💩 😺 😸 😹 😻 😼 😽 🙀 😿 😾 👶 👧 🧒 👦 👩 🧑 👨 👵 🧓 👴 👲 👳‍♀️ 👳‍♂️ 🧕 🧔 👱‍♂️ 👱‍♀️ 👨‍🦰 👩‍🦰 👨‍🦱 👩‍🦱 👨‍🦲 👩‍🦲 👨‍🦳 👩‍🦳 🦸‍♀️ 🦸‍♂️ 🦹‍♀️ 🦹‍♂️ 👮‍♀️ 👮‍♂️ 👷‍♀️ 👷‍♂️ 💂‍♀️ 💂‍♂️ 🕵️‍♀️ 🕵️‍♂️ 👩‍⚕️ 👨‍⚕️ 👩‍🌾 👨‍🌾 👩‍🍳 👨‍🍳 👩‍🎓 👨‍🎓 👩‍🎤 👨‍🎤 👩‍🏫 👨‍🏫 👩‍🏭 👨‍🏭 👩‍💻 👨‍💻 👩‍💼 👨‍💼 👩‍🔧 👨‍🔧 👩‍🔬 👨‍🔬 👩‍🎨 👨‍🎨 👩‍🚒 👨‍🚒 👩‍✈️ 👨‍✈️ 👩‍🚀 👨‍🚀 👩‍⚖️ 👨‍⚖️ 👰 🤵 👸 🤴 🤶 🎅 🧙‍♀️ 🧙‍♂️ 🧝‍♀️ 🧝‍♂️ 🧛‍♀️ 🧛‍♂️ 🧟‍♀️ 🧟‍♂️ 🧞‍♀️ 🧞‍♂️ 🧜‍♀️ 🧜‍♂️ 🧚‍♀️ 🧚‍♂️ 👼 🤰 🤱 🙇‍♀️ 🙇‍♂️ 💁‍♀️ 💁‍♂️ 🙅‍♀️ 🙅‍♂️ 🙆‍♀️ 🙆‍♂️ 🙋‍♀️ 🙋‍♂️ 🤦‍♀️ 🤦‍♂️ 🤷‍♀️ 🤷‍♂️ 🙎‍♀️ 🙎‍♂️ 🙍‍♀️ 🙍‍♂️ 💇‍♀️ 💇‍♂️ 💆‍♀️ 💆‍♂️ 🧖‍♀️ 🧖‍♂️ 💅 🤳 💃 🕺 👯‍♀️ 👯‍♂️ 🕴 🚶‍♀️ 🚶‍♂️ 🏃‍♀️ 🏃‍♂️ 👫 👭 👬 💑 👩‍❤️‍👩 👨‍❤️‍👨 💏 👩‍❤️‍💋‍👩 👨‍❤️‍💋‍👨 👪 👨‍👩‍👧 👨‍👩‍👧‍👦 👨‍👩‍👦‍👦 👨‍👩‍👧‍👧 👩‍👩‍👦 👩‍👩‍👧 👩‍👩‍👧‍👦 👩‍👩‍👦‍👦 👩‍👩‍👧‍👧 👨‍👨‍👦 👨‍👨‍👧 👨‍👨‍👧‍👦 👨‍👨‍👦‍👦 👨‍👨‍👧‍👧 👩‍👦 👩‍👧 👩‍👧‍👦 👩‍👦‍👦 👩‍👧‍👧 👨‍👦 👨‍👧 👨‍👧‍👦 👨‍👦‍👦 👨‍👧‍👧 🤲 👐 🙌 👏 🤝 👍 👎 👊 ✊ 🤛 🤜 🤞 ✌️ 🤟 🤘 👌 👈 👉 👆 👇 ☝️ ✋ 🤚 🖐 🖖 👋 🤙 💪 🦵 🦶 🖕 ✍️ 🙏 💍 💄 💋 👄 👅 👂 👃 👣 👁 👀 🧠 🦴 🦷 🗣 👤 👥 @@ -53,7 +66,37 @@ In respect that the structure of the sufficient amount poses problems and challe 🥱 🤏 🦾 🦿 🦻 🧏 🧏‍♂️ 🧏‍♀️ 🧍 🧍‍♂️ 🧍‍♀️ 🧎 🧎‍♂️ 🧎‍♀️ 👨‍🦯 👩‍🦯 👨‍🦼 👩‍🦼 👨‍🦽 👩‍🦽 🦧 🦮 🐕‍🦺 🦥 🦦 🦨 🦩 🧄 🧅 🧇 🧆 🧈 🦪 🧃 🧉 🧊 🛕 🦽 🦼 🛺 🪂 🪐 🤿 🪀 🪁 🦺 🥻 🩱 🩲 🩳 🩰 🪕 🪔 🪓 🦯 🩸 🩹 🩺 🪑 🪒 🤎 🤍 🟠 🟡 🟢 🟣 🟤 🟥 🟧 🟨 🟩 🟦 🟪 🟫"; [Benchmark] - public TextLayout BuildEmojisTextLayout() => new TextLayout(Emojis, Typeface.Default, 12d, Brushes.Black); + public TextLayout BuildEmojisTextLayout() => MakeLayout(Emojis); + + [Benchmark] + public TextLayout[] BuildManySmallTexts() => _manySmallStrings.Select(MakeLayout).ToArray(); + + [Benchmark] + public void VirtualizeTextBlocks() + { + var blocks = new TextBlock[10]; + for (var i = 0; i < blocks.Length; i++) + { + blocks[i] = new TextBlock + { + Width = 120, + Height = 32, + }; + } + + for (int i = 0, j = 0; i < _manySmallStrings.Length; i++, j = j < blocks.Length - 1 ? j + 1 : 0) + { + blocks[j].Text = _manySmallStrings[i]; + blocks[j].Measure(new Size(200, 200)); + } + } + + private static TextLayout MakeLayout(string str) + { + var layout = new TextLayout(str, Typeface.Default, 12d, Brushes.Black, maxWidth: 120); + layout.Dispose(); + return layout; + } public void Dispose() { diff --git a/tests/Avalonia.Controls.UnitTests/Presenters/TextPresenter_Tests.cs b/tests/Avalonia.Controls.UnitTests/Presenters/TextPresenter_Tests.cs index 8e06fbd831..61ce056c49 100644 --- a/tests/Avalonia.Controls.UnitTests/Presenters/TextPresenter_Tests.cs +++ b/tests/Avalonia.Controls.UnitTests/Presenters/TextPresenter_Tests.cs @@ -46,7 +46,7 @@ namespace Avalonia.Controls.UnitTests.Presenters Assert.NotNull(target.TextLayout); var actual = string.Join(null, - target.TextLayout.TextLines.SelectMany(x => x.TextRuns).Select(x => x.CharacterBufferReference.CharacterBuffer.Span.ToString())); + target.TextLayout.TextLines.SelectMany(x => x.TextRuns).Select(x => x.Text.ToString())); Assert.Equal("****", actual); } diff --git a/tests/Avalonia.Controls.UnitTests/WindowTests.cs b/tests/Avalonia.Controls.UnitTests/WindowTests.cs index 9dc0cb95bd..ca245005c2 100644 --- a/tests/Avalonia.Controls.UnitTests/WindowTests.cs +++ b/tests/Avalonia.Controls.UnitTests/WindowTests.cs @@ -140,7 +140,7 @@ namespace Avalonia.Controls.UnitTests [Theory] [InlineData(true)] [InlineData(false)] - public void Child_windows_should_be_closed_before_parent(bool programaticClose) + public void Child_windows_should_be_closed_before_parent(bool programmaticClose) { using (UnitTestApplication.Start(TestServices.StyledWindow)) { @@ -156,7 +156,7 @@ namespace Avalonia.Controls.UnitTests window.Closing += (sender, e) => { Assert.Equal(WindowCloseReason.WindowClosing, e.CloseReason); - Assert.Equal(programaticClose, e.IsProgrammatic); + Assert.Equal(programmaticClose, e.IsProgrammatic); count++; windowClosing = count; }; @@ -164,7 +164,7 @@ namespace Avalonia.Controls.UnitTests child.Closing += (sender, e) => { Assert.Equal(WindowCloseReason.OwnerWindowClosing, e.CloseReason); - Assert.Equal(programaticClose, e.IsProgrammatic); + Assert.Equal(programmaticClose, e.IsProgrammatic); count++; childClosing = count; }; @@ -184,7 +184,7 @@ namespace Avalonia.Controls.UnitTests window.Show(); child.Show(window); - if (programaticClose) + if (programmaticClose) { window.Close(); } @@ -205,7 +205,7 @@ namespace Avalonia.Controls.UnitTests [Theory] [InlineData(true)] [InlineData(false)] - public void Child_windows_must_not_close_before_parent_has_chance_to_Cancel_OSCloseButton(bool programaticClose) + public void Child_windows_must_not_close_before_parent_has_chance_to_Cancel_OSCloseButton(bool programmaticClose) { using (UnitTestApplication.Start(TestServices.StyledWindow)) { @@ -246,7 +246,7 @@ namespace Avalonia.Controls.UnitTests window.Show(); child.Show(window); - if (programaticClose) + if (programmaticClose) { window.Close(); } diff --git a/tests/Avalonia.DesignerSupport.Tests/Remote/HtmlTransport/webapp/package-lock.json b/tests/Avalonia.DesignerSupport.Tests/Remote/HtmlTransport/webapp/package-lock.json index 0c0857b680..0b92805712 100644 --- a/tests/Avalonia.DesignerSupport.Tests/Remote/HtmlTransport/webapp/package-lock.json +++ b/tests/Avalonia.DesignerSupport.Tests/Remote/HtmlTransport/webapp/package-lock.json @@ -1,8 +1,2671 @@ { "name": "simple-test", "version": "1.0.0", - "lockfileVersion": 1, + "lockfileVersion": 2, "requires": true, + "packages": { + "": { + "name": "simple-test", + "version": "1.0.0", + "license": "MIT", + "devDependencies": { + "@types/chai": "4.2.12", + "@types/mocha": "8.0.3", + "@types/react": "^16.3.14", + "chai": "^4.2.0", + "mocha": "^10.2.0", + "moq.ts": "^6.4.0", + "nyc": "^15.1.0", + "react": "^16.3.2", + "ts-node": "^9.0.0", + "typescript": "^4.0.2" + } + }, + "node_modules/@babel/code-frame": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.10.4.tgz", + "integrity": "sha512-vG6SvB6oYEhvgisZNFRmRCUkLz11c7rp+tbNTynGqc6mS1d5ATd/sGyV6W0KZZnXRKMTzZDRgQT3Ou9jhpAfUg==", + "dev": true, + "dependencies": { + "@babel/highlight": "^7.10.4" + } + }, + "node_modules/@babel/core": { + "version": "7.11.6", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.11.6.tgz", + "integrity": "sha512-Wpcv03AGnmkgm6uS6k8iwhIwTrcP0m17TL1n1sy7qD0qelDu4XNeW0dN0mHfa+Gei211yDaLoEe/VlbXQzM4Bg==", + "dev": true, + "dependencies": { + "@babel/code-frame": "^7.10.4", + "@babel/generator": "^7.11.6", + "@babel/helper-module-transforms": "^7.11.0", + "@babel/helpers": "^7.10.4", + "@babel/parser": "^7.11.5", + "@babel/template": "^7.10.4", + "@babel/traverse": "^7.11.5", + "@babel/types": "^7.11.5", + "convert-source-map": "^1.7.0", + "debug": "^4.1.0", + "gensync": "^1.0.0-beta.1", + "json5": "^2.1.2", + "lodash": "^4.17.19", + "resolve": "^1.3.2", + "semver": "^5.4.1", + "source-map": "^0.5.0" + }, + "engines": { + "node": ">=6.9.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/babel" + } + }, + "node_modules/@babel/core/node_modules/semver": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", + "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==", + "dev": true, + "bin": { + "semver": "bin/semver" + } + }, + "node_modules/@babel/generator": { + "version": "7.11.6", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.11.6.tgz", + "integrity": "sha512-DWtQ1PV3r+cLbySoHrwn9RWEgKMBLLma4OBQloPRyDYvc5msJM9kvTLo1YnlJd1P/ZuKbdli3ijr5q3FvAF3uA==", + "dev": true, + "dependencies": { + "@babel/types": "^7.11.5", + "jsesc": "^2.5.1", + "source-map": "^0.5.0" + } + }, + "node_modules/@babel/helper-function-name": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.10.4.tgz", + "integrity": "sha512-YdaSyz1n8gY44EmN7x44zBn9zQ1Ry2Y+3GTA+3vH6Mizke1Vw0aWDM66FOYEPw8//qKkmqOckrGgTYa+6sceqQ==", + "dev": true, + "dependencies": { + "@babel/helper-get-function-arity": "^7.10.4", + "@babel/template": "^7.10.4", + "@babel/types": "^7.10.4" + } + }, + "node_modules/@babel/helper-get-function-arity": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/helper-get-function-arity/-/helper-get-function-arity-7.10.4.tgz", + "integrity": "sha512-EkN3YDB+SRDgiIUnNgcmiD361ti+AVbL3f3Henf6dqqUyr5dMsorno0lJWJuLhDhkI5sYEpgj6y9kB8AOU1I2A==", + "dev": true, + "dependencies": { + "@babel/types": "^7.10.4" + } + }, + "node_modules/@babel/helper-member-expression-to-functions": { + "version": "7.11.0", + "resolved": "https://registry.npmjs.org/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.11.0.tgz", + "integrity": "sha512-JbFlKHFntRV5qKw3YC0CvQnDZ4XMwgzzBbld7Ly4Mj4cbFy3KywcR8NtNctRToMWJOVvLINJv525Gd6wwVEx/Q==", + "dev": true, + "dependencies": { + "@babel/types": "^7.11.0" + } + }, + "node_modules/@babel/helper-module-imports": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.10.4.tgz", + "integrity": "sha512-nEQJHqYavI217oD9+s5MUBzk6x1IlvoS9WTPfgG43CbMEeStE0v+r+TucWdx8KFGowPGvyOkDT9+7DHedIDnVw==", + "dev": true, + "dependencies": { + "@babel/types": "^7.10.4" + } + }, + "node_modules/@babel/helper-module-transforms": { + "version": "7.11.0", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.11.0.tgz", + "integrity": "sha512-02EVu8COMuTRO1TAzdMtpBPbe6aQ1w/8fePD2YgQmxZU4gpNWaL9gK3Jp7dxlkUlUCJOTaSeA+Hrm1BRQwqIhg==", + "dev": true, + "dependencies": { + "@babel/helper-module-imports": "^7.10.4", + "@babel/helper-replace-supers": "^7.10.4", + "@babel/helper-simple-access": "^7.10.4", + "@babel/helper-split-export-declaration": "^7.11.0", + "@babel/template": "^7.10.4", + "@babel/types": "^7.11.0", + "lodash": "^4.17.19" + } + }, + "node_modules/@babel/helper-optimise-call-expression": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.10.4.tgz", + "integrity": "sha512-n3UGKY4VXwXThEiKrgRAoVPBMqeoPgHVqiHZOanAJCG9nQUL2pLRQirUzl0ioKclHGpGqRgIOkgcIJaIWLpygg==", + "dev": true, + "dependencies": { + "@babel/types": "^7.10.4" + } + }, + "node_modules/@babel/helper-replace-supers": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/helper-replace-supers/-/helper-replace-supers-7.10.4.tgz", + "integrity": "sha512-sPxZfFXocEymYTdVK1UNmFPBN+Hv5mJkLPsYWwGBxZAxaWfFu+xqp7b6qWD0yjNuNL2VKc6L5M18tOXUP7NU0A==", + "dev": true, + "dependencies": { + "@babel/helper-member-expression-to-functions": "^7.10.4", + "@babel/helper-optimise-call-expression": "^7.10.4", + "@babel/traverse": "^7.10.4", + "@babel/types": "^7.10.4" + } + }, + "node_modules/@babel/helper-simple-access": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/helper-simple-access/-/helper-simple-access-7.10.4.tgz", + "integrity": "sha512-0fMy72ej/VEvF8ULmX6yb5MtHG4uH4Dbd6I/aHDb/JVg0bbivwt9Wg+h3uMvX+QSFtwr5MeItvazbrc4jtRAXw==", + "dev": true, + "dependencies": { + "@babel/template": "^7.10.4", + "@babel/types": "^7.10.4" + } + }, + "node_modules/@babel/helper-split-export-declaration": { + "version": "7.11.0", + "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.11.0.tgz", + "integrity": "sha512-74Vejvp6mHkGE+m+k5vHY93FX2cAtrw1zXrZXRlG4l410Nm9PxfEiVTn1PjDPV5SnmieiueY4AFg2xqhNFuuZg==", + "dev": true, + "dependencies": { + "@babel/types": "^7.11.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.10.4.tgz", + "integrity": "sha512-3U9y+43hz7ZM+rzG24Qe2mufW5KhvFg/NhnNph+i9mgCtdTCtMJuI1TMkrIUiK7Ix4PYlRF9I5dhqaLYA/ADXw==", + "dev": true + }, + "node_modules/@babel/helpers": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.10.4.tgz", + "integrity": "sha512-L2gX/XeUONeEbI78dXSrJzGdz4GQ+ZTA/aazfUsFaWjSe95kiCuOZ5HsXvkiw3iwF+mFHSRUfJU8t6YavocdXA==", + "dev": true, + "dependencies": { + "@babel/template": "^7.10.4", + "@babel/traverse": "^7.10.4", + "@babel/types": "^7.10.4" + } + }, + "node_modules/@babel/highlight": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.10.4.tgz", + "integrity": "sha512-i6rgnR/YgPEQzZZnbTHHuZdlE8qyoBNalD6F+q4vAFlcMEcqmkoG+mPqJYJCo63qPf74+Y1UZsl3l6f7/RIkmA==", + "dev": true, + "dependencies": { + "@babel/helper-validator-identifier": "^7.10.4", + "chalk": "^2.0.0", + "js-tokens": "^4.0.0" + } + }, + "node_modules/@babel/highlight/node_modules/ansi-styles": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", + "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", + "dev": true, + "dependencies": { + "color-convert": "^1.9.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/@babel/highlight/node_modules/chalk": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", + "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "dev": true, + "dependencies": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/@babel/highlight/node_modules/color-convert": { + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", + "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", + "dev": true, + "dependencies": { + "color-name": "1.1.3" + } + }, + "node_modules/@babel/highlight/node_modules/color-name": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", + "integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=", + "dev": true + }, + "node_modules/@babel/highlight/node_modules/escape-string-regexp": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=", + "dev": true, + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/@babel/highlight/node_modules/has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/@babel/highlight/node_modules/supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "dev": true, + "dependencies": { + "has-flag": "^3.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/@babel/parser": { + "version": "7.11.5", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.11.5.tgz", + "integrity": "sha512-X9rD8qqm695vgmeaQ4fvz/o3+Wk4ZzQvSHkDBgpYKxpD4qTAUm88ZKtHkVqIOsYFFbIQ6wQYhC6q7pjqVK0E0Q==", + "dev": true, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/template": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.10.4.tgz", + "integrity": "sha512-ZCjD27cGJFUB6nmCB1Enki3r+L5kJveX9pq1SvAUKoICy6CZ9yD8xO086YXdYhvNjBdnekm4ZnaP5yC8Cs/1tA==", + "dev": true, + "dependencies": { + "@babel/code-frame": "^7.10.4", + "@babel/parser": "^7.10.4", + "@babel/types": "^7.10.4" + } + }, + "node_modules/@babel/traverse": { + "version": "7.11.5", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.11.5.tgz", + "integrity": "sha512-EjiPXt+r7LiCZXEfRpSJd+jUMnBd4/9OUv7Nx3+0u9+eimMwJmG0Q98lw4/289JCoxSE8OolDMNZaaF/JZ69WQ==", + "dev": true, + "dependencies": { + "@babel/code-frame": "^7.10.4", + "@babel/generator": "^7.11.5", + "@babel/helper-function-name": "^7.10.4", + "@babel/helper-split-export-declaration": "^7.11.0", + "@babel/parser": "^7.11.5", + "@babel/types": "^7.11.5", + "debug": "^4.1.0", + "globals": "^11.1.0", + "lodash": "^4.17.19" + } + }, + "node_modules/@babel/types": { + "version": "7.11.5", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.11.5.tgz", + "integrity": "sha512-bvM7Qz6eKnJVFIn+1LPtjlBFPVN5jNDc1XmN15vWe7Q3DPBufWWsLiIvUu7xW87uTG6QoggpIDnUgLQvPheU+Q==", + "dev": true, + "dependencies": { + "@babel/helper-validator-identifier": "^7.10.4", + "lodash": "^4.17.19", + "to-fast-properties": "^2.0.0" + } + }, + "node_modules/@istanbuljs/load-nyc-config": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@istanbuljs/load-nyc-config/-/load-nyc-config-1.1.0.tgz", + "integrity": "sha512-VjeHSlIzpv/NyD3N0YuHfXOPDIixcA1q2ZV98wsMqcYlPmv2n3Yb2lYP9XMElnaFVXg5A7YLTeLu6V84uQDjmQ==", + "dev": true, + "dependencies": { + "camelcase": "^5.3.1", + "find-up": "^4.1.0", + "get-package-type": "^0.1.0", + "js-yaml": "^3.13.1", + "resolve-from": "^5.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@istanbuljs/load-nyc-config/node_modules/find-up": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", + "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", + "dev": true, + "dependencies": { + "locate-path": "^5.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@istanbuljs/load-nyc-config/node_modules/locate-path": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", + "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", + "dev": true, + "dependencies": { + "p-locate": "^4.1.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@istanbuljs/load-nyc-config/node_modules/p-limit": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", + "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "dev": true, + "dependencies": { + "p-try": "^2.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@istanbuljs/load-nyc-config/node_modules/p-locate": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", + "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", + "dev": true, + "dependencies": { + "p-limit": "^2.2.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@istanbuljs/schema": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/@istanbuljs/schema/-/schema-0.1.2.tgz", + "integrity": "sha512-tsAQNx32a8CoFhjhijUIhI4kccIAgmGhy8LZMZgGfmXcpMbPRUqn5LWmgRttILi6yeGmBJd2xsPkFMs0PzgPCw==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/@types/chai": { + "version": "4.2.12", + "resolved": "https://registry.npmjs.org/@types/chai/-/chai-4.2.12.tgz", + "integrity": "sha512-aN5IAC8QNtSUdQzxu7lGBgYAOuU1tmRU4c9dIq5OKGf/SBVjXo+ffM2wEjudAWbgpOhy60nLoAGH1xm8fpCKFQ==", + "dev": true + }, + "node_modules/@types/color-name": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@types/color-name/-/color-name-1.1.1.tgz", + "integrity": "sha512-rr+OQyAjxze7GgWrSaJwydHStIhHq2lvY3BOC2Mj7KnzI7XK0Uw1TOOdI9lDoajEbSWLiYgoo4f1R51erQfhPQ==", + "dev": true + }, + "node_modules/@types/mocha": { + "version": "8.0.3", + "resolved": "https://registry.npmjs.org/@types/mocha/-/mocha-8.0.3.tgz", + "integrity": "sha512-vyxR57nv8NfcU0GZu8EUXZLTbCMupIUwy95LJ6lllN+JRPG25CwMHoB1q5xKh8YKhQnHYRAn4yW2yuHbf/5xgg==", + "dev": true + }, + "node_modules/@types/prop-types": { + "version": "15.7.3", + "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.3.tgz", + "integrity": "sha512-KfRL3PuHmqQLOG+2tGpRO26Ctg+Cq1E01D2DMriKEATHgWLfeNDmq9e29Q9WIky0dQ3NPkd1mzYH8Lm936Z9qw==", + "dev": true + }, + "node_modules/@types/react": { + "version": "16.9.49", + "resolved": "https://registry.npmjs.org/@types/react/-/react-16.9.49.tgz", + "integrity": "sha512-DtLFjSj0OYAdVLBbyjhuV9CdGVHCkHn2R+xr3XkBvK2rS1Y1tkc14XSGjYgm5Fjjr90AxH9tiSzc1pCFMGO06g==", + "dev": true, + "dependencies": { + "@types/prop-types": "*", + "csstype": "^3.0.2" + } + }, + "node_modules/aggregate-error": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/aggregate-error/-/aggregate-error-3.1.0.tgz", + "integrity": "sha512-4I7Td01quW/RpocfNayFdFVk1qSuoh0E7JrbRJ16nH01HhKFQ88INq9Sd+nd72zqRySlr9BmDA8xlEJ6vJMrYA==", + "dev": true, + "dependencies": { + "clean-stack": "^2.0.0", + "indent-string": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/ansi-colors": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-4.1.1.tgz", + "integrity": "sha512-JoX0apGbHaUJBNl6yF+p6JAFYZ666/hhCGKN5t9QFjbJQKUU/g8MNbFDbvfrgKXvI1QpZplPOnwIo99lX/AAmA==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/ansi-styles": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.2.1.tgz", + "integrity": "sha512-9VGjrMsG1vePxcSweQsN20KY/c4zN0h9fLjqAbwbPfahM3t+NL+M9HC8xeXG2I8pX5NoamTGNuomEUFI7fcUjA==", + "dev": true, + "dependencies": { + "@types/color-name": "^1.1.1", + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/anymatch": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", + "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", + "dev": true, + "dependencies": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/append-transform": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/append-transform/-/append-transform-2.0.0.tgz", + "integrity": "sha512-7yeyCEurROLQJFv5Xj4lEGTy0borxepjFv1g22oAdqFu//SrAlDl1O1Nxx15SH1RoliUml6p8dwJW9jvZughhg==", + "dev": true, + "dependencies": { + "default-require-extensions": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/archy": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/archy/-/archy-1.0.0.tgz", + "integrity": "sha1-+cjBN1fMHde8N5rHeyxipcKGjEA=", + "dev": true + }, + "node_modules/arg": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/arg/-/arg-4.1.3.tgz", + "integrity": "sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==", + "dev": true + }, + "node_modules/argparse": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", + "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", + "dev": true, + "dependencies": { + "sprintf-js": "~1.0.2" + } + }, + "node_modules/assertion-error": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-1.1.0.tgz", + "integrity": "sha512-jgsaNduz+ndvGyFt3uSuWqvy4lCnIJiovtouQN5JZHOKCS2QuhEdbcQHFhVksz2N2U9hXJo8odG7ETyWlEeuDw==", + "dev": true, + "engines": { + "node": "*" + } + }, + "node_modules/balanced-match": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz", + "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=", + "dev": true + }, + "node_modules/binary-extensions": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.2.0.tgz", + "integrity": "sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/braces": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", + "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", + "dev": true, + "dependencies": { + "fill-range": "^7.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/browser-stdout": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/browser-stdout/-/browser-stdout-1.3.1.tgz", + "integrity": "sha512-qhAVI1+Av2X7qelOfAIYwXONood6XlZE/fXaBSmW/T5SzLAmCgzi+eiWE7fUvbHaeNBQH13UftjpXxsfLkMpgw==", + "dev": true + }, + "node_modules/buffer-from": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.1.tgz", + "integrity": "sha512-MQcXEUbCKtEo7bhqEs6560Hyd4XaovZlO/k9V3hjVUF/zwW7KBVdSK4gIt/bzwS9MbR5qob+F5jusZsb0YQK2A==", + "dev": true + }, + "node_modules/caching-transform": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/caching-transform/-/caching-transform-4.0.0.tgz", + "integrity": "sha512-kpqOvwXnjjN44D89K5ccQC+RUrsy7jB/XLlRrx0D7/2HNcTPqzsb6XgYoErwko6QsV184CA2YgS1fxDiiDZMWA==", + "dev": true, + "dependencies": { + "hasha": "^5.0.0", + "make-dir": "^3.0.0", + "package-hash": "^4.0.0", + "write-file-atomic": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/camelcase": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", + "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/chai": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/chai/-/chai-4.2.0.tgz", + "integrity": "sha512-XQU3bhBukrOsQCuwZndwGcCVQHyZi53fQ6Ys1Fym7E4olpIqqZZhhoFJoaKVvV17lWQoXYwgWN2nF5crA8J2jw==", + "dev": true, + "dependencies": { + "assertion-error": "^1.1.0", + "check-error": "^1.0.2", + "deep-eql": "^3.0.1", + "get-func-name": "^2.0.0", + "pathval": "^1.1.0", + "type-detect": "^4.0.5" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/check-error": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/check-error/-/check-error-1.0.2.tgz", + "integrity": "sha1-V00xLt2Iu13YkS6Sht1sCu1KrII=", + "dev": true, + "engines": { + "node": "*" + } + }, + "node_modules/chokidar": { + "version": "3.5.3", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.5.3.tgz", + "integrity": "sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw==", + "dev": true, + "funding": [ + { + "type": "individual", + "url": "https://paulmillr.com/funding/" + } + ], + "dependencies": { + "anymatch": "~3.1.2", + "braces": "~3.0.2", + "glob-parent": "~5.1.2", + "is-binary-path": "~2.1.0", + "is-glob": "~4.0.1", + "normalize-path": "~3.0.0", + "readdirp": "~3.6.0" + }, + "engines": { + "node": ">= 8.10.0" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + } + }, + "node_modules/clean-stack": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/clean-stack/-/clean-stack-2.2.0.tgz", + "integrity": "sha512-4diC9HaTE+KRAMWhDhrGOECgWZxoevMc5TlkObMqNSsVU62PYzXZ/SMTjzyGAFF1YusgxGcSWTEXBhp0CPwQ1A==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/cliui": { + "version": "7.0.4", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-7.0.4.tgz", + "integrity": "sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ==", + "dev": true, + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.0", + "wrap-ansi": "^7.0.0" + } + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "node_modules/commondir": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/commondir/-/commondir-1.0.1.tgz", + "integrity": "sha1-3dgA2gxmEnOTzKWVDqloo6rxJTs=", + "dev": true + }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=", + "dev": true + }, + "node_modules/convert-source-map": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.7.0.tgz", + "integrity": "sha512-4FJkXzKXEDB1snCFZlLP4gpC3JILicCpGbzG9f9G7tGqGCzETQ2hWPrcinA9oU4wtf2biUaEH5065UnMeR33oA==", + "dev": true, + "dependencies": { + "safe-buffer": "~5.1.1" + } + }, + "node_modules/convert-source-map/node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "dev": true + }, + "node_modules/cross-spawn": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", + "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==", + "dev": true, + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/csstype": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.0.3.tgz", + "integrity": "sha512-jPl+wbWPOWJ7SXsWyqGRk3lGecbar0Cb0OvZF/r/ZU011R4YqiRehgkQ9p4eQfo9DSDLqLL3wHwfxeJiuIsNag==", + "dev": true + }, + "node_modules/debug": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz", + "integrity": "sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==", + "deprecated": "Debug versions >=3.2.0 <3.2.7 || >=4 <4.3.1 have a low-severity ReDos regression when used in a Node.js environment. It is recommended you upgrade to 3.2.7 or 4.3.1. (https://github.com/visionmedia/debug/issues/797)", + "dev": true, + "dependencies": { + "ms": "^2.1.1" + } + }, + "node_modules/decamelize": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz", + "integrity": "sha1-9lNNFRSCabIDUue+4m9QH5oZEpA=", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/deep-eql": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-3.0.1.tgz", + "integrity": "sha512-+QeIQyN5ZuO+3Uk5DYh6/1eKO0m0YmJFGNmFHGACpf1ClL1nmlV/p4gNgbl2pJGxgXb4faqo6UE+M5ACEMyVcw==", + "dev": true, + "dependencies": { + "type-detect": "^4.0.0" + }, + "engines": { + "node": ">=0.12" + } + }, + "node_modules/default-require-extensions": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/default-require-extensions/-/default-require-extensions-3.0.0.tgz", + "integrity": "sha512-ek6DpXq/SCpvjhpFsLFRVtIxJCRw6fUR42lYMVZuUMK7n8eMz4Uh5clckdBjEpLhn/gEBZo7hDJnJcwdKLKQjg==", + "dev": true, + "dependencies": { + "strip-bom": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/diff": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz", + "integrity": "sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==", + "dev": true, + "engines": { + "node": ">=0.3.1" + } + }, + "node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true + }, + "node_modules/es6-error": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/es6-error/-/es6-error-4.1.1.tgz", + "integrity": "sha512-Um/+FxMr9CISWh0bi5Zv0iOD+4cFh5qLeks1qhAopKVAJw3drgKbKySikp7wGhDL0HPeaja0P5ULZrxLkniUVg==", + "dev": true + }, + "node_modules/escalade": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.1.tgz", + "integrity": "sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/esprima": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", + "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", + "dev": true, + "bin": { + "esparse": "bin/esparse.js", + "esvalidate": "bin/esvalidate.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/fill-range": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", + "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", + "dev": true, + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/find-cache-dir": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/find-cache-dir/-/find-cache-dir-3.3.1.tgz", + "integrity": "sha512-t2GDMt3oGC/v+BMwzmllWDuJF/xcDtE5j/fCGbqDD7OLuJkj0cfh1YSA5VKPvwMeLFLNDBkwOKZ2X85jGLVftQ==", + "dev": true, + "dependencies": { + "commondir": "^1.0.1", + "make-dir": "^3.0.2", + "pkg-dir": "^4.1.0" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/avajs/find-cache-dir?sponsor=1" + } + }, + "node_modules/find-up": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", + "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", + "dev": true, + "dependencies": { + "locate-path": "^6.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/flat": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/flat/-/flat-5.0.2.tgz", + "integrity": "sha512-b6suED+5/3rTpUBdG1gupIl8MPFCAMA0QXwmljLhvCUKcUvdE4gWky9zpuGCcXHOsz4J9wPGNWq6OKpmIzz3hQ==", + "dev": true, + "bin": { + "flat": "cli.js" + } + }, + "node_modules/foreground-child": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-2.0.0.tgz", + "integrity": "sha512-dCIq9FpEcyQyXKCkyzmlPTFNgrCzPudOe+mhvJU5zAtlBnGVy2yKxtfsxK2tQBThwq225jcvBjpw1Gr40uzZCA==", + "dev": true, + "dependencies": { + "cross-spawn": "^7.0.0", + "signal-exit": "^3.0.2" + }, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/fromentries": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/fromentries/-/fromentries-1.2.1.tgz", + "integrity": "sha512-Xu2Qh8yqYuDhQGOhD5iJGninErSfI9A3FrriD3tjUgV5VbJFeH8vfgZ9HnC6jWN80QDVNQK5vmxRAmEAp7Mevw==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, + "node_modules/fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=", + "dev": true + }, + "node_modules/fsevents": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", + "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", + "dev": true, + "hasInstallScript": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/gensync": { + "version": "1.0.0-beta.1", + "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.1.tgz", + "integrity": "sha512-r8EC6NO1sngH/zdD9fiRDLdcgnbayXah+mLgManTaIZJqEC1MZstmnox8KpnI2/fxQwrp5OpCOYWLp4rBl4Jcg==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "dev": true, + "engines": { + "node": "6.* || 8.* || >= 10.*" + } + }, + "node_modules/get-func-name": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/get-func-name/-/get-func-name-2.0.0.tgz", + "integrity": "sha1-6td0q+5y4gQJQzoGY2YCPdaIekE=", + "dev": true, + "engines": { + "node": "*" + } + }, + "node_modules/get-package-type": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/get-package-type/-/get-package-type-0.1.0.tgz", + "integrity": "sha512-pjzuKtY64GYfWizNAJ0fr9VqttZkNiK2iS430LtIHzjBEr6bX8Am2zm4sW4Ro5wjWW5cAlRL1qAMTcXbjNAO2Q==", + "dev": true, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/glob": { + "version": "7.1.6", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.6.tgz", + "integrity": "sha512-LwaxwyZ72Lk7vZINtNNrywX0ZuLyStrdDtabefZKAY5ZGJhVtgdznluResxNmPitE0SAO+O26sWTHeKSI2wMBA==", + "dev": true, + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.0.4", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/globals": { + "version": "11.12.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz", + "integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/graceful-fs": { + "version": "4.2.4", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.4.tgz", + "integrity": "sha512-WjKPNJF79dtJAVniUlGGWHYGz2jWxT6VhN/4m1NdkbZ2nOsEF+cI1Edgql5zCRhs/VsQYRvrXctxktVXZUkixw==", + "dev": true + }, + "node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/hasha": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/hasha/-/hasha-5.2.0.tgz", + "integrity": "sha512-2W+jKdQbAdSIrggA8Q35Br8qKadTrqCTC8+XZvBWepKDK6m9XkX6Iz1a2yh2KP01kzAR/dpuMeUnocoLYDcskw==", + "dev": true, + "dependencies": { + "is-stream": "^2.0.0", + "type-fest": "^0.8.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/he": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/he/-/he-1.2.0.tgz", + "integrity": "sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==", + "dev": true, + "bin": { + "he": "bin/he" + } + }, + "node_modules/html-escaper": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz", + "integrity": "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==", + "dev": true + }, + "node_modules/imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha1-khi5srkoojixPcT7a21XbyMUU+o=", + "dev": true, + "engines": { + "node": ">=0.8.19" + } + }, + "node_modules/indent-string": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-4.0.0.tgz", + "integrity": "sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=", + "dev": true, + "dependencies": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "dev": true + }, + "node_modules/is-binary-path": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", + "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", + "dev": true, + "dependencies": { + "binary-extensions": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true, + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/is-plain-obj": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-2.1.0.tgz", + "integrity": "sha512-YWnfyRwxL/+SsrWYfOpUtz5b3YD+nyfkHvjbcanzk8zgyO4ASD67uVMRt8k5bM4lLMDnXfriRhOpemw+NfT1eA==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/is-stream": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.0.tgz", + "integrity": "sha512-XCoy+WlUr7d1+Z8GgSuXmpuUFC9fOhRXglJMx+dwLKTkL44Cjd4W1Z5P+BQZpr+cR93aGP4S/s7Ftw6Nd/kiEw==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/is-typedarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz", + "integrity": "sha1-5HnICFjfDBsR3dppQPlgEfzaSpo=", + "dev": true + }, + "node_modules/is-unicode-supported": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-0.1.0.tgz", + "integrity": "sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-windows": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-windows/-/is-windows-1.0.2.tgz", + "integrity": "sha512-eXK1UInq2bPmjyX6e3VHIzMLobc4J94i4AWn+Hpq3OU5KkrRC96OAcR3PRJ/pGu6m8TRnBHP9dkXQVsT/COVIA==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha1-6PvzdNxVb/iUehDcsFctYz8s+hA=", + "dev": true + }, + "node_modules/istanbul-lib-coverage": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-3.0.0.tgz", + "integrity": "sha512-UiUIqxMgRDET6eR+o5HbfRYP1l0hqkWOs7vNxC/mggutCMUIhWMm8gAHb8tHlyfD3/l6rlgNA5cKdDzEAf6hEg==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/istanbul-lib-hook": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/istanbul-lib-hook/-/istanbul-lib-hook-3.0.0.tgz", + "integrity": "sha512-Pt/uge1Q9s+5VAZ+pCo16TYMWPBIl+oaNIjgLQxcX0itS6ueeaA+pEfThZpH8WxhFgCiEb8sAJY6MdUKgiIWaQ==", + "dev": true, + "dependencies": { + "append-transform": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/istanbul-lib-instrument": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-4.0.3.tgz", + "integrity": "sha512-BXgQl9kf4WTCPCCpmFGoJkz/+uhvm7h7PFKUYxh7qarQd3ER33vHG//qaE8eN25l07YqZPpHXU9I09l/RD5aGQ==", + "dev": true, + "dependencies": { + "@babel/core": "^7.7.5", + "@istanbuljs/schema": "^0.1.2", + "istanbul-lib-coverage": "^3.0.0", + "semver": "^6.3.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/istanbul-lib-processinfo": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/istanbul-lib-processinfo/-/istanbul-lib-processinfo-2.0.2.tgz", + "integrity": "sha512-kOwpa7z9hme+IBPZMzQ5vdQj8srYgAtaRqeI48NGmAQ+/5yKiHLV0QbYqQpxsdEF0+w14SoB8YbnHKcXE2KnYw==", + "dev": true, + "dependencies": { + "archy": "^1.0.0", + "cross-spawn": "^7.0.0", + "istanbul-lib-coverage": "^3.0.0-alpha.1", + "make-dir": "^3.0.0", + "p-map": "^3.0.0", + "rimraf": "^3.0.0", + "uuid": "^3.3.3" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/istanbul-lib-report": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/istanbul-lib-report/-/istanbul-lib-report-3.0.0.tgz", + "integrity": "sha512-wcdi+uAKzfiGT2abPpKZ0hSU1rGQjUQnLvtY5MpQ7QCTahD3VODhcu4wcfY1YtkGaDD5yuydOLINXsfbus9ROw==", + "dev": true, + "dependencies": { + "istanbul-lib-coverage": "^3.0.0", + "make-dir": "^3.0.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/istanbul-lib-source-maps": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/istanbul-lib-source-maps/-/istanbul-lib-source-maps-4.0.0.tgz", + "integrity": "sha512-c16LpFRkR8vQXyHZ5nLpY35JZtzj1PQY1iZmesUbf1FZHbIupcWfjgOXBY9YHkLEQ6puz1u4Dgj6qmU/DisrZg==", + "dev": true, + "dependencies": { + "debug": "^4.1.1", + "istanbul-lib-coverage": "^3.0.0", + "source-map": "^0.6.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/istanbul-lib-source-maps/node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/istanbul-reports": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-3.0.2.tgz", + "integrity": "sha512-9tZvz7AiR3PEDNGiV9vIouQ/EAcqMXFmkcA1CDFTwOB98OZVDL0PH9glHotf5Ugp6GCOTypfzGWI/OqjWNCRUw==", + "dev": true, + "dependencies": { + "html-escaper": "^2.0.0", + "istanbul-lib-report": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "dev": true + }, + "node_modules/js-yaml": { + "version": "3.14.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.0.tgz", + "integrity": "sha512-/4IbIeHcD9VMHFqDR/gQ7EdZdLimOvW2DdcxFjdyyZ9NsbS+ccrXqVWDtab/lRl5AlUqmpBx8EhPaWR+OtY17A==", + "dev": true, + "dependencies": { + "argparse": "^1.0.7", + "esprima": "^4.0.0" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/jsesc": { + "version": "2.5.2", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-2.5.2.tgz", + "integrity": "sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA==", + "dev": true, + "bin": { + "jsesc": "bin/jsesc" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/json5": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.2.tgz", + "integrity": "sha512-46Tk9JiOL2z7ytNQWFLpj99RZkVgeHf87yGQKsIkaPz1qSH9UczKH1rO7K3wgRselo0tYMUNfecYpm/p1vC7tQ==", + "dev": true, + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/locate-path": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", + "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", + "dev": true, + "dependencies": { + "p-locate": "^5.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/lodash": { + "version": "4.17.20", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.20.tgz", + "integrity": "sha512-PlhdFcillOINfeV7Ni6oF1TAEayyZBoZ8bcshTHqOYJYlrqzRK5hagpagky5o4HfCzzd1TRkXPMFq6cKk9rGmA==", + "dev": true + }, + "node_modules/lodash.flattendeep": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/lodash.flattendeep/-/lodash.flattendeep-4.4.0.tgz", + "integrity": "sha1-+wMJF/hqMTTlvJvsDWngAT3f7bI=", + "dev": true + }, + "node_modules/log-symbols": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-4.1.0.tgz", + "integrity": "sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg==", + "dev": true, + "dependencies": { + "chalk": "^4.1.0", + "is-unicode-supported": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/loose-envify": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", + "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", + "dev": true, + "dependencies": { + "js-tokens": "^3.0.0 || ^4.0.0" + }, + "bin": { + "loose-envify": "cli.js" + } + }, + "node_modules/make-dir": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-3.1.0.tgz", + "integrity": "sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw==", + "dev": true, + "dependencies": { + "semver": "^6.0.0" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/make-error": { + "version": "1.3.6", + "resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz", + "integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==", + "dev": true + }, + "node_modules/minimatch": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", + "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==", + "dev": true, + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/mocha": { + "version": "10.2.0", + "resolved": "https://registry.npmjs.org/mocha/-/mocha-10.2.0.tgz", + "integrity": "sha512-IDY7fl/BecMwFHzoqF2sg/SHHANeBoMMXFlS9r0OXKDssYE1M5O43wUY/9BVPeIvfH2zmEbBfseqN9gBQZzXkg==", + "dev": true, + "dependencies": { + "ansi-colors": "4.1.1", + "browser-stdout": "1.3.1", + "chokidar": "3.5.3", + "debug": "4.3.4", + "diff": "5.0.0", + "escape-string-regexp": "4.0.0", + "find-up": "5.0.0", + "glob": "7.2.0", + "he": "1.2.0", + "js-yaml": "4.1.0", + "log-symbols": "4.1.0", + "minimatch": "5.0.1", + "ms": "2.1.3", + "nanoid": "3.3.3", + "serialize-javascript": "6.0.0", + "strip-json-comments": "3.1.1", + "supports-color": "8.1.1", + "workerpool": "6.2.1", + "yargs": "16.2.0", + "yargs-parser": "20.2.4", + "yargs-unparser": "2.0.0" + }, + "bin": { + "_mocha": "bin/_mocha", + "mocha": "bin/mocha.js" + }, + "engines": { + "node": ">= 14.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/mochajs" + } + }, + "node_modules/mocha/node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "dev": true + }, + "node_modules/mocha/node_modules/debug": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "dev": true, + "dependencies": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/mocha/node_modules/debug/node_modules/ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "dev": true + }, + "node_modules/mocha/node_modules/diff": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/diff/-/diff-5.0.0.tgz", + "integrity": "sha512-/VTCrvm5Z0JGty/BWHljh+BAiw3IK+2j87NGMu8Nwc/f48WoDAC395uomO9ZD117ZOBaHmkX1oyLvkVM/aIT3w==", + "dev": true, + "engines": { + "node": ">=0.3.1" + } + }, + "node_modules/mocha/node_modules/glob": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.0.tgz", + "integrity": "sha512-lmLf6gtyrPq8tTjSmrO94wBeQbFR3HbLHbuyD69wuyQkImp2hWqMGB47OX65FBkPffO641IP9jWa1z4ivqG26Q==", + "dev": true, + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.0.4", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/mocha/node_modules/glob/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/mocha/node_modules/js-yaml": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", + "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", + "dev": true, + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/mocha/node_modules/minimatch": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.0.1.tgz", + "integrity": "sha512-nLDxIFRyhDblz3qMuq+SoRZED4+miJ/G+tdDrjkkkRnjAsBexeGpgjLEQ0blJy7rHhR2b93rhQY4SvyWu9v03g==", + "dev": true, + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/mocha/node_modules/minimatch/node_modules/brace-expansion": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", + "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/mocha/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true + }, + "node_modules/mocha/node_modules/supports-color": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", + "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", + "dev": true, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/supports-color?sponsor=1" + } + }, + "node_modules/moq.ts": { + "version": "6.4.0", + "resolved": "https://registry.npmjs.org/moq.ts/-/moq.ts-6.4.0.tgz", + "integrity": "sha512-g9siyKOvYGVhfDCl9mpLptch93egRC3yXIbPLssxukOROl368XBB+6M7YjLTkjVRHbKbpJBD6P7od+Ky/0xWIw==", + "dev": true, + "dependencies": { + "tslib": "^2.0.0" + } + }, + "node_modules/ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "dev": true + }, + "node_modules/nanoid": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.3.tgz", + "integrity": "sha512-p1sjXuopFs0xg+fPASzQ28agW1oHD7xDsd9Xkf3T15H3c/cifrFHVwrh74PdoklAPi+i7MdRsE47vm2r6JoB+w==", + "dev": true, + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/node-preload": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/node-preload/-/node-preload-0.2.1.tgz", + "integrity": "sha512-RM5oyBy45cLEoHqCeh+MNuFAxO0vTFBLskvQbOKnEE7YTTSN4tbN8QWDIPQ6L+WvKsB/qLEGpYe2ZZ9d4W9OIQ==", + "dev": true, + "dependencies": { + "process-on-spawn": "^1.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/nyc": { + "version": "15.1.0", + "resolved": "https://registry.npmjs.org/nyc/-/nyc-15.1.0.tgz", + "integrity": "sha512-jMW04n9SxKdKi1ZMGhvUTHBN0EICCRkHemEoE5jm6mTYcqcdas0ATzgUgejlQUHMvpnOZqGB5Xxsv9KxJW1j8A==", + "dev": true, + "dependencies": { + "@istanbuljs/load-nyc-config": "^1.0.0", + "@istanbuljs/schema": "^0.1.2", + "caching-transform": "^4.0.0", + "convert-source-map": "^1.7.0", + "decamelize": "^1.2.0", + "find-cache-dir": "^3.2.0", + "find-up": "^4.1.0", + "foreground-child": "^2.0.0", + "get-package-type": "^0.1.0", + "glob": "^7.1.6", + "istanbul-lib-coverage": "^3.0.0", + "istanbul-lib-hook": "^3.0.0", + "istanbul-lib-instrument": "^4.0.0", + "istanbul-lib-processinfo": "^2.0.2", + "istanbul-lib-report": "^3.0.0", + "istanbul-lib-source-maps": "^4.0.0", + "istanbul-reports": "^3.0.2", + "make-dir": "^3.0.0", + "node-preload": "^0.2.1", + "p-map": "^3.0.0", + "process-on-spawn": "^1.0.0", + "resolve-from": "^5.0.0", + "rimraf": "^3.0.0", + "signal-exit": "^3.0.2", + "spawn-wrap": "^2.0.0", + "test-exclude": "^6.0.0", + "yargs": "^15.0.2" + }, + "bin": { + "nyc": "bin/nyc.js" + }, + "engines": { + "node": ">=8.9" + } + }, + "node_modules/nyc/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/nyc/node_modules/cliui": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-6.0.0.tgz", + "integrity": "sha512-t6wbgtoCXvAzst7QgXxJYqPt0usEfbgQdftEPbLL/cvv6HPE5VgvqCuAIDR0NgU52ds6rFwqrgakNLrHEjCbrQ==", + "dev": true, + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.0", + "wrap-ansi": "^6.2.0" + } + }, + "node_modules/nyc/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true + }, + "node_modules/nyc/node_modules/find-up": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", + "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", + "dev": true, + "dependencies": { + "locate-path": "^5.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/nyc/node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/nyc/node_modules/locate-path": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", + "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", + "dev": true, + "dependencies": { + "p-locate": "^4.1.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/nyc/node_modules/p-limit": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", + "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "dev": true, + "dependencies": { + "p-try": "^2.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/nyc/node_modules/p-locate": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", + "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", + "dev": true, + "dependencies": { + "p-limit": "^2.2.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/nyc/node_modules/string-width": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.0.tgz", + "integrity": "sha512-zUz5JD+tgqtuDjMhwIg5uFVV3dtqZ9yQJlZVfq4I01/K5Paj5UHj7VyrQOJvzawSVlKpObApbfD0Ed6yJc+1eg==", + "dev": true, + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/nyc/node_modules/strip-ansi": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.0.tgz", + "integrity": "sha512-AuvKTrTfQNYNIctbR1K/YGTR1756GycPsg7b9bdV9Duqur4gv6aKqHXah67Z8ImS7WEz5QVcOtlfW2rZEugt6w==", + "dev": true, + "dependencies": { + "ansi-regex": "^5.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/nyc/node_modules/wrap-ansi": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-6.2.0.tgz", + "integrity": "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/nyc/node_modules/yargs": { + "version": "15.4.1", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-15.4.1.tgz", + "integrity": "sha512-aePbxDmcYW++PaqBsJ+HYUFwCdv4LVvdnhBy78E57PIor8/OVvhMrADFFEDh8DHDFRv/O9i3lPhsENjO7QX0+A==", + "dev": true, + "dependencies": { + "cliui": "^6.0.0", + "decamelize": "^1.2.0", + "find-up": "^4.1.0", + "get-caller-file": "^2.0.1", + "require-directory": "^2.1.1", + "require-main-filename": "^2.0.0", + "set-blocking": "^2.0.0", + "string-width": "^4.2.0", + "which-module": "^2.0.0", + "y18n": "^4.0.0", + "yargs-parser": "^18.1.2" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/nyc/node_modules/yargs-parser": { + "version": "18.1.3", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-18.1.3.tgz", + "integrity": "sha512-o50j0JeToy/4K6OZcaQmW6lyXXKhq7csREXcDwk2omFPJEwUNOVtJKvmDr9EI1fAJZUyZcRF7kxGBWmRXudrCQ==", + "dev": true, + "dependencies": { + "camelcase": "^5.0.0", + "decamelize": "^1.2.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM=", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", + "dev": true, + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "dev": true, + "dependencies": { + "yocto-queue": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-locate": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", + "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", + "dev": true, + "dependencies": { + "p-limit": "^3.0.2" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-map": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/p-map/-/p-map-3.0.0.tgz", + "integrity": "sha512-d3qXVTF/s+W+CdJ5A29wywV2n8CQQYahlgz2bFiA+4eVNJbHJodPZ+/gXwPGh0bOqA+j8S+6+ckmvLGPk1QpxQ==", + "dev": true, + "dependencies": { + "aggregate-error": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/p-try": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", + "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/package-hash": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/package-hash/-/package-hash-4.0.0.tgz", + "integrity": "sha512-whdkPIooSu/bASggZ96BWVvZTRMOFxnyUG5PnTSGKoJE2gd5mbVNmR2Nj20QFzxYYgAXpoqC+AiXzl+UMRh7zQ==", + "dev": true, + "dependencies": { + "graceful-fs": "^4.1.15", + "hasha": "^5.0.0", + "lodash.flattendeep": "^4.4.0", + "release-zalgo": "^1.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/path-parse": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.6.tgz", + "integrity": "sha512-GSmOT2EbHrINBf9SR7CDELwlJ8AENk3Qn7OikK4nFYAu3Ote2+JYNVvkpAEQm3/TLNEJFD/xZJjzyxg3KBWOzw==", + "dev": true + }, + "node_modules/pathval": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/pathval/-/pathval-1.1.1.tgz", + "integrity": "sha512-Dp6zGqpTdETdR63lehJYPeIOqpiNBNtc7BpWSLrOje7UaIsE5aY92r/AunQA7rsXvet3lrJ3JnZX29UPTKXyKQ==", + "dev": true, + "engines": { + "node": "*" + } + }, + "node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true, + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/pkg-dir": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-4.2.0.tgz", + "integrity": "sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==", + "dev": true, + "dependencies": { + "find-up": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/pkg-dir/node_modules/find-up": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", + "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", + "dev": true, + "dependencies": { + "locate-path": "^5.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/pkg-dir/node_modules/locate-path": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", + "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", + "dev": true, + "dependencies": { + "p-locate": "^4.1.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/pkg-dir/node_modules/p-limit": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", + "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "dev": true, + "dependencies": { + "p-try": "^2.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/pkg-dir/node_modules/p-locate": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", + "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", + "dev": true, + "dependencies": { + "p-limit": "^2.2.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/process-on-spawn": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/process-on-spawn/-/process-on-spawn-1.0.0.tgz", + "integrity": "sha512-1WsPDsUSMmZH5LeMLegqkPDrsGgsWwk1Exipy2hvB0o/F0ASzbpIctSCcZIK1ykJvtTJULEH+20WOFjMvGnCTg==", + "dev": true, + "dependencies": { + "fromentries": "^1.2.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/prop-types": { + "version": "15.7.2", + "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.7.2.tgz", + "integrity": "sha512-8QQikdH7//R2vurIJSutZ1smHYTcLpRWEOlHnzcWHmBYrOGUysKwSsrC89BCiFj3CbrfJ/nXFdJepOVrY1GCHQ==", + "dev": true, + "dependencies": { + "loose-envify": "^1.4.0", + "object-assign": "^4.1.1", + "react-is": "^16.8.1" + } + }, + "node_modules/randombytes": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz", + "integrity": "sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==", + "dev": true, + "dependencies": { + "safe-buffer": "^5.1.0" + } + }, + "node_modules/react": { + "version": "16.13.1", + "resolved": "https://registry.npmjs.org/react/-/react-16.13.1.tgz", + "integrity": "sha512-YMZQQq32xHLX0bz5Mnibv1/LHb3Sqzngu7xstSM+vrkE5Kzr9xE0yMByK5kMoTK30YVJE61WfbxIFFvfeDKT1w==", + "dev": true, + "dependencies": { + "loose-envify": "^1.1.0", + "object-assign": "^4.1.1", + "prop-types": "^15.6.2" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-is": { + "version": "16.13.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", + "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==", + "dev": true + }, + "node_modules/readdirp": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", + "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", + "dev": true, + "dependencies": { + "picomatch": "^2.2.1" + }, + "engines": { + "node": ">=8.10.0" + } + }, + "node_modules/release-zalgo": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/release-zalgo/-/release-zalgo-1.0.0.tgz", + "integrity": "sha1-CXALflB0Mpc5Mw5TXFqQ+2eFFzA=", + "dev": true, + "dependencies": { + "es6-error": "^4.0.1" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/require-directory": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha1-jGStX9MNqxyXbiNE/+f3kqam30I=", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/require-main-filename": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/require-main-filename/-/require-main-filename-2.0.0.tgz", + "integrity": "sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg==", + "dev": true + }, + "node_modules/resolve": { + "version": "1.17.0", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.17.0.tgz", + "integrity": "sha512-ic+7JYiV8Vi2yzQGFWOkiZD5Z9z7O2Zhm9XMaTxdJExKasieFCr+yXZ/WmXsckHiKl12ar0y6XiXDx3m4RHn1w==", + "dev": true, + "dependencies": { + "path-parse": "^1.0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/resolve-from": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz", + "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/rimraf": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", + "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", + "dev": true, + "dependencies": { + "glob": "^7.1.3" + }, + "bin": { + "rimraf": "bin.js" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, + "node_modules/semver": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", + "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", + "dev": true, + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/serialize-javascript": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-6.0.0.tgz", + "integrity": "sha512-Qr3TosvguFt8ePWqsvRfrKyQXIiW+nGbYpy8XK24NQHE83caxWt+mIymTT19DGFbNWNLfEwsrkSmN64lVWB9ag==", + "dev": true, + "dependencies": { + "randombytes": "^2.1.0" + } + }, + "node_modules/set-blocking": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz", + "integrity": "sha1-BF+XgtARrppoA93TgrJDkrPYkPc=", + "dev": true + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/signal-exit": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.3.tgz", + "integrity": "sha512-VUJ49FC8U1OxwZLxIbTTrDvLnf/6TDgxZcK8wxR8zs13xpx7xbG60ndBlhNrFi2EMuFRoeDoJO7wthSLq42EjA==", + "dev": true + }, + "node_modules/source-map": { + "version": "0.5.7", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", + "integrity": "sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w=", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/source-map-support": { + "version": "0.5.19", + "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.19.tgz", + "integrity": "sha512-Wonm7zOCIJzBGQdB+thsPar0kYuCIzYvxZwlBa87yi/Mdjv7Tip2cyVbLj5o0cFPN4EVkuTwb3GDDyUx2DGnGw==", + "dev": true, + "dependencies": { + "buffer-from": "^1.0.0", + "source-map": "^0.6.0" + } + }, + "node_modules/source-map-support/node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/spawn-wrap": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/spawn-wrap/-/spawn-wrap-2.0.0.tgz", + "integrity": "sha512-EeajNjfN9zMnULLwhZZQU3GWBoFNkbngTUPfaawT4RkMiviTxcX0qfhVbGey39mfctfDHkWtuecgQ8NJcyQWHg==", + "dev": true, + "dependencies": { + "foreground-child": "^2.0.0", + "is-windows": "^1.0.2", + "make-dir": "^3.0.0", + "rimraf": "^3.0.0", + "signal-exit": "^3.0.2", + "which": "^2.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/sprintf-js": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", + "integrity": "sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw=", + "dev": true + }, + "node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-bom": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-4.0.0.tgz", + "integrity": "sha512-3xurFv5tEgii33Zi8Jtp55wEIILR9eh34FAW00PZf+JnSsTmV/ioewSgQl97JHvgjoRGwPShsWm+IdrxB35d0w==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-json-comments": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", + "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", + "dev": true, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/supports-color": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.1.0.tgz", + "integrity": "sha512-oRSIpR8pxT1Wr2FquTNnGet79b3BWljqOuoW/h4oBhxJ/HUbX5nX6JSruTkvXDCFMwDPvsaTTbvMLKZWSy0R5g==", + "dev": true, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/test-exclude": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-6.0.0.tgz", + "integrity": "sha512-cAGWPIyOHU6zlmg88jwm7VRyXnMN7iV68OGAbYDk/Mh/xC/pzVPlQtY6ngoIH/5/tciuhGfvESU8GrHrcxD56w==", + "dev": true, + "dependencies": { + "@istanbuljs/schema": "^0.1.2", + "glob": "^7.1.4", + "minimatch": "^3.0.4" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/to-fast-properties": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-2.0.0.tgz", + "integrity": "sha1-3F5pjL0HkmW8c+A3doGk5Og/YW4=", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/ts-node": { + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-9.0.0.tgz", + "integrity": "sha512-/TqB4SnererCDR/vb4S/QvSZvzQMJN8daAslg7MeaiHvD8rDZsSfXmNeNumyZZzMned72Xoq/isQljYSt8Ynfg==", + "dev": true, + "dependencies": { + "arg": "^4.1.0", + "diff": "^4.0.1", + "make-error": "^1.1.1", + "source-map-support": "^0.5.17", + "yn": "3.1.1" + }, + "bin": { + "ts-node": "dist/bin.js", + "ts-node-script": "dist/bin-script.js", + "ts-node-transpile-only": "dist/bin-transpile.js", + "ts-script": "dist/bin-script-deprecated.js" + }, + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "typescript": ">=2.7" + } + }, + "node_modules/tslib": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.0.1.tgz", + "integrity": "sha512-SgIkNheinmEBgx1IUNirK0TUD4X9yjjBRTqqjggWCU3pUEqIk3/Uwl3yRixYKT6WjQuGiwDv4NomL3wqRCj+CQ==", + "dev": true + }, + "node_modules/type-detect": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.0.8.tgz", + "integrity": "sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/type-fest": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.8.1.tgz", + "integrity": "sha512-4dbzIzqvjtgiM5rw1k5rEHtBANKmdudhGyBEajN01fEyhaAIhsoKNy6y7+IN93IfpFtwY9iqi7kD+xwKhQsNJA==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/typedarray-to-buffer": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/typedarray-to-buffer/-/typedarray-to-buffer-3.1.5.tgz", + "integrity": "sha512-zdu8XMNEDepKKR+XYOXAVPtWui0ly0NtohUscw+UmaHiAWT8hrV1rr//H6V+0DvJ3OQ19S979M0laLfX8rm82Q==", + "dev": true, + "dependencies": { + "is-typedarray": "^1.0.0" + } + }, + "node_modules/typescript": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.0.2.tgz", + "integrity": "sha512-e4ERvRV2wb+rRZ/IQeb3jm2VxBsirQLpQhdxplZ2MEzGvDkkMmPglecnNDfSUBivMjP93vRbngYYDQqQ/78bcQ==", + "dev": true, + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=4.2.0" + } + }, + "node_modules/uuid": { + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.4.0.tgz", + "integrity": "sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A==", + "deprecated": "Please upgrade to version 7 or higher. Older versions may use Math.random() in certain circumstances, which is known to be problematic. See https://v8.dev/blog/math-random for details.", + "dev": true, + "bin": { + "uuid": "bin/uuid" + } + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/which-module": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/which-module/-/which-module-2.0.0.tgz", + "integrity": "sha1-2e8H3Od7mQK4o6j6SzHD4/fm6Ho=", + "dev": true + }, + "node_modules/workerpool": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/workerpool/-/workerpool-6.2.1.tgz", + "integrity": "sha512-ILEIE97kDZvF9Wb9f6h5aXK4swSlKGUcOEGiIYb2OOu/IrDU9iwj0fD//SsA6E5ibwJxpEvhullJY4Sl4GcpAw==", + "dev": true + }, + "node_modules/wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=", + "dev": true + }, + "node_modules/write-file-atomic": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-3.0.3.tgz", + "integrity": "sha512-AvHcyZ5JnSfq3ioSyjrBkH9yW4m7Ayk8/9My/DD9onKeu/94fwrMocemO2QAJFAlnnDN+ZDS+ZjAR5ua1/PV/Q==", + "dev": true, + "dependencies": { + "imurmurhash": "^0.1.4", + "is-typedarray": "^1.0.0", + "signal-exit": "^3.0.2", + "typedarray-to-buffer": "^3.1.5" + } + }, + "node_modules/y18n": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-4.0.0.tgz", + "integrity": "sha512-r9S/ZyXu/Xu9q1tYlpsLIsa3EeLXXk0VwlxqTcFRfg9EhMW+17kbt9G0NrgCmhGb5vT2hyhJZLfDGx+7+5Uj/w==", + "dev": true + }, + "node_modules/yargs": { + "version": "16.2.0", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-16.2.0.tgz", + "integrity": "sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw==", + "dev": true, + "dependencies": { + "cliui": "^7.0.2", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.0", + "y18n": "^5.0.5", + "yargs-parser": "^20.2.2" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/yargs-parser": { + "version": "20.2.4", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-20.2.4.tgz", + "integrity": "sha512-WOkpgNhPTlE73h4VFAFsOnomJVaovO8VqLDzy5saChRBFQFBoMYirowyW+Q9HB4HFF4Z7VZTiG3iSzJJA29yRA==", + "dev": true, + "engines": { + "node": ">=10" + } + }, + "node_modules/yargs-unparser": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/yargs-unparser/-/yargs-unparser-2.0.0.tgz", + "integrity": "sha512-7pRTIA9Qc1caZ0bZ6RYRGbHJthJWuakf+WmHK0rVeLkNrrGhfoabBNdue6kdINI6r4if7ocq9aD/n7xwKOdzOA==", + "dev": true, + "dependencies": { + "camelcase": "^6.0.0", + "decamelize": "^4.0.0", + "flat": "^5.0.2", + "is-plain-obj": "^2.1.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/yargs-unparser/node_modules/camelcase": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.3.0.tgz", + "integrity": "sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/yargs-unparser/node_modules/decamelize": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-4.0.0.tgz", + "integrity": "sha512-9iE1PgSik9HeIIw2JO94IidnE3eBoQrFJ3w7sFuzSX4DpmZ3v5sZpUiV5Swcf6mQEF+Y0ru8Neo+p+nyh2J+hQ==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/yargs/node_modules/y18n": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", + "dev": true, + "engines": { + "node": ">=10" + } + }, + "node_modules/yn": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz", + "integrity": "sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/yocto-queue": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", + "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + } + }, "dependencies": { "@babel/code-frame": { "version": "7.10.4", diff --git a/tests/Avalonia.Markup.Xaml.UnitTests/Converters/GeometryTypeConverterTests.cs b/tests/Avalonia.Markup.Xaml.UnitTests/Converters/GeometryTypeConverterTests.cs new file mode 100644 index 0000000000..329a14afa6 --- /dev/null +++ b/tests/Avalonia.Markup.Xaml.UnitTests/Converters/GeometryTypeConverterTests.cs @@ -0,0 +1,54 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Avalonia.Controls; +using Avalonia.Controls.Shapes; +using Avalonia.Media; +using Avalonia.UnitTests; +using Xunit; + +namespace Avalonia.Markup.Xaml.UnitTests.Converters +{ + public class GeometryTypeConverterTests: XamlTestBase + { + public class StringDataViewModel + { + public string PathData { get; set; } + } + + public class IntDataViewModel + { + public int PathData { get; set; } + } + + + [Theory] + [MemberData(nameof(Get_GeometryTypeConverter_Data))] + public void GeometryTypeConverter_Value_Work(object vm, bool nullData) + { + using(UnitTestApplication.Start(TestServices.StyledWindow)) + { + var xaml = @" + + +"; + var window = (Window)AvaloniaRuntimeXamlLoader.Load(xaml); + var path = window.FindControl("path"); + window.DataContext = vm; + Assert.Equal(nullData, path.Data is null); + } + } + + public static IEnumerable Get_GeometryTypeConverter_Data() + { + yield return new object[] { new StringDataViewModel { }, true }; + yield return new object[] { new StringDataViewModel { PathData = "M406.39,333.45l205.93,0" }, false }; + yield return new object[] { new IntDataViewModel { }, true }; + yield return new object[] { new IntDataViewModel { PathData = 100 }, true }; + } + } +} diff --git a/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/BasicTests.cs b/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/BasicTests.cs index 18a6dd9803..0cdc9ee3b1 100644 --- a/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/BasicTests.cs +++ b/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/BasicTests.cs @@ -59,7 +59,7 @@ namespace Avalonia.Markup.Xaml.UnitTests.Xaml } [Fact] - public void Attached_Property_Is_Set_On_Control_Outside_Avalonia_Namspace() + public void Attached_Property_Is_Set_On_Control_Outside_Avalonia_Namespace() { // Test for issue #1548 var xaml = @@ -729,7 +729,7 @@ namespace Avalonia.Markup.Xaml.UnitTests.Xaml } [Fact] - public void DeferedXamlLoader_Should_Preserve_NamespacesContext() + public void DeferredXamlLoader_Should_Preserve_NamespacesContext() { var xaml = @" - - + + - - + + @@ -41,7 +41,7 @@ namespace Avalonia.Markup.Xaml.UnitTests.Xaml window.ApplyTemplate(); button.ApplyTemplate(); - var listBoxHierarchyLine = button.GetVisualChildren().ElementAt(0) as ListBoxHierachyLine; + var listBoxHierarchyLine = button.GetVisualChildren().ElementAt(0) as ListBoxHierarchyLine; Assert.Equal(1, listBoxHierarchyLine.LineDashStyle.Offset); Assert.Equal(2, listBoxHierarchyLine.LineDashStyle.Dashes.Count); Assert.Equal(2, listBoxHierarchyLine.LineDashStyle.Dashes[0]); @@ -309,10 +309,10 @@ namespace Avalonia.Markup.Xaml.UnitTests.Xaml Assert.Equal("Bar", bar.Name); } } - public class ListBoxHierachyLine : Panel + public class ListBoxHierarchyLine : Panel { public static readonly StyledProperty LineDashStyleProperty = - AvaloniaProperty.Register(nameof(LineDashStyle)); + AvaloniaProperty.Register(nameof(LineDashStyle)); public DashStyle LineDashStyle { diff --git a/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/StyleTests.cs b/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/StyleTests.cs index 70cf3c8276..8a1ee72c54 100644 --- a/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/StyleTests.cs +++ b/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/StyleTests.cs @@ -272,7 +272,7 @@ namespace Avalonia.Markup.Xaml.UnitTests.Xaml } [Fact] - public void Style_Can_Use_NthChild_Selector_After_Reoder() + public void Style_Can_Use_NthChild_Selector_After_Reorder() { using (UnitTestApplication.Start(TestServices.StyledWindow)) { @@ -311,7 +311,7 @@ namespace Avalonia.Markup.Xaml.UnitTests.Xaml } [Fact] - public void Style_Can_Use_NthLastChild_Selector_After_Reoder() + public void Style_Can_Use_NthLastChild_Selector_After_Reorder() { using (UnitTestApplication.Start(TestServices.StyledWindow)) { diff --git a/tests/Avalonia.RenderTests/Controls/TextBlockTests.cs b/tests/Avalonia.RenderTests/Controls/TextBlockTests.cs index bddde3c3c6..c11bd2b816 100644 --- a/tests/Avalonia.RenderTests/Controls/TextBlockTests.cs +++ b/tests/Avalonia.RenderTests/Controls/TextBlockTests.cs @@ -45,7 +45,7 @@ namespace Avalonia.Direct2D1.RenderTests.Controls [Win32Fact("Has text")] public async Task RestrictedHeight_VerticalAlign() { - Control text(VerticalAlignment verticalAlingnment, bool clip = true, bool restrictHeight = true) + Control text(VerticalAlignment verticalAlignment, bool clip = true, bool restrictHeight = true) { return new Border() { @@ -62,7 +62,7 @@ namespace Avalonia.Direct2D1.RenderTests.Controls FontSize = 24, Foreground = Brushes.Black, Text = "L", - VerticalAlignment = verticalAlingnment, + VerticalAlignment = verticalAlignment, ClipToBounds = clip } }; diff --git a/tests/Avalonia.RenderTests/Media/TextFormatting/TextLayoutTests.cs b/tests/Avalonia.RenderTests/Media/TextFormatting/TextLayoutTests.cs index f921d9fa64..6f47aa58d8 100644 --- a/tests/Avalonia.RenderTests/Media/TextFormatting/TextLayoutTests.cs +++ b/tests/Avalonia.RenderTests/Media/TextFormatting/TextLayoutTests.cs @@ -186,7 +186,7 @@ namespace Avalonia.Direct2D1.RenderTests.Media [InlineData("x", 0, 200, 200 - 7.20, 0, 7.20, FontSizeHeight)] [InlineData(stringword, 0, 200, 171.20, 0, 7.20, FontSizeHeight)] [InlineData(stringword, 3, 200, 200 - 7.20, 0, 7.20, FontSizeHeight)] - public void Should_HitTestPosition_RigthAlign_Correctly( + public void Should_HitTestPosition_RightAlign_Correctly( string input, int index, double widthConstraint, double x, double y, double width, double height) { diff --git a/tests/Avalonia.RenderTests/OpacityMaskTests.cs b/tests/Avalonia.RenderTests/OpacityMaskTests.cs index 2f01b03db6..6ce7e72df1 100644 --- a/tests/Avalonia.RenderTests/OpacityMaskTests.cs +++ b/tests/Avalonia.RenderTests/OpacityMaskTests.cs @@ -57,7 +57,7 @@ namespace Avalonia.Direct2D1.RenderTests } [Fact] - public async Task RenderTansform_Applies_To_Opacity_Mask() + public async Task RenderTransform_Applies_To_Opacity_Mask() { var target = new Canvas { diff --git a/tests/Avalonia.Skia.UnitTests/Media/GlyphRunTests.cs b/tests/Avalonia.Skia.UnitTests/Media/GlyphRunTests.cs index 4083a67b5e..04bc401479 100644 --- a/tests/Avalonia.Skia.UnitTests/Media/GlyphRunTests.cs +++ b/tests/Avalonia.Skia.UnitTests/Media/GlyphRunTests.cs @@ -19,7 +19,7 @@ namespace Avalonia.Skia.UnitTests.Media { var options = new TextShaperOptions(Typeface.Default.GlyphTypeface, 10, direction, CultureInfo.CurrentCulture); var shapedBuffer = - TextShaper.Current.ShapeText(new CharacterBufferReference(text), text.Length, options); + TextShaper.Current.ShapeText(text, options); var glyphRun = CreateGlyphRun(shapedBuffer); @@ -60,7 +60,7 @@ namespace Avalonia.Skia.UnitTests.Media { var options = new TextShaperOptions(Typeface.Default.GlyphTypeface, 10, direction, CultureInfo.CurrentCulture); var shapedBuffer = - TextShaper.Current.ShapeText(new CharacterBufferReference(text), text.Length, options); + TextShaper.Current.ShapeText(text, options); var glyphRun = CreateGlyphRun(shapedBuffer); @@ -103,7 +103,7 @@ namespace Avalonia.Skia.UnitTests.Media { var options = new TextShaperOptions(Typeface.Default.GlyphTypeface, 10, direction, CultureInfo.CurrentCulture); var shapedBuffer = - TextShaper.Current.ShapeText(new CharacterBufferReference(text), text.Length, options); + TextShaper.Current.ShapeText(text, options); var glyphRun = CreateGlyphRun(shapedBuffer); @@ -112,14 +112,14 @@ namespace Avalonia.Skia.UnitTests.Media var characterHit = glyphRun.GetCharacterHitFromDistance(glyphRun.Metrics.WidthIncludingTrailingWhitespace, out _); - Assert.Equal(glyphRun.Characters.Count, characterHit.FirstCharacterIndex + characterHit.TrailingLength); + Assert.Equal(glyphRun.Characters.Length, characterHit.FirstCharacterIndex + characterHit.TrailingLength); } else { var characterHit = glyphRun.GetCharacterHitFromDistance(0, out _); - Assert.Equal(glyphRun.Characters.Count, characterHit.FirstCharacterIndex + characterHit.TrailingLength); + Assert.Equal(glyphRun.Characters.Length, characterHit.FirstCharacterIndex + characterHit.TrailingLength); } var rects = BuildRects(glyphRun); @@ -215,7 +215,7 @@ namespace Avalonia.Skia.UnitTests.Media var glyphRun = new GlyphRun( shapedBuffer.GlyphTypeface, shapedBuffer.FontRenderingEmSize, - shapedBuffer.CharacterBufferRange, + shapedBuffer.Text, shapedBuffer.GlyphIndices, shapedBuffer.GlyphAdvances, shapedBuffer.GlyphOffsets, diff --git a/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/SingleBufferTextSource.cs b/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/SingleBufferTextSource.cs index f12f42bd5e..f963277397 100644 --- a/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/SingleBufferTextSource.cs +++ b/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/SingleBufferTextSource.cs @@ -1,15 +1,16 @@ -using Avalonia.Media.TextFormatting; +using System; +using Avalonia.Media.TextFormatting; namespace Avalonia.Skia.UnitTests.Media.TextFormatting { internal class SingleBufferTextSource : ITextSource { - private readonly CharacterBufferRange _text; + private readonly string _text; private readonly GenericTextRunProperties _defaultGenericPropertiesRunProperties; public SingleBufferTextSource(string text, GenericTextRunProperties defaultProperties) { - _text = new CharacterBufferRange(text); + _text = text; _defaultGenericPropertiesRunProperties = defaultProperties; } @@ -20,14 +21,14 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting return null; } - var runText = _text.Skip(textSourceIndex); + var runText = _text.AsMemory(textSourceIndex); if (runText.IsEmpty) { return null; } - return new TextCharacters(runText.CharacterBufferReference, runText.Length, _defaultGenericPropertiesRunProperties); + return new TextCharacters(runText, _defaultGenericPropertiesRunProperties); } } } diff --git a/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextFormatterTests.cs b/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextFormatterTests.cs index 1b6fd537eb..b90752861c 100644 --- a/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextFormatterTests.cs +++ b/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextFormatterTests.cs @@ -279,7 +279,7 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting { using (Start()) { - var lineBreaker = new LineBreakEnumerator(new CharacterBufferRange(text)); + var lineBreaker = new LineBreakEnumerator(text); var expected = new List(); @@ -677,7 +677,7 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting return new RectangleRun(new Rect(0, 0, 50, 50), Brushes.Green); } - return new TextCharacters(_text, 0, _text.Length, new GenericTextRunProperties(Typeface.Default, foregroundBrush: Brushes.Black)); + return new TextCharacters(_text, new GenericTextRunProperties(Typeface.Default, foregroundBrush: Brushes.Black)); } } diff --git a/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextLayoutTests.cs b/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextLayoutTests.cs index 7501bf21fe..1fd26748cd 100644 --- a/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextLayoutTests.cs +++ b/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextLayoutTests.cs @@ -2,6 +2,7 @@ using System.Collections.Generic; using System.Globalization; using System.Linq; +using System.Runtime.InteropServices; using Avalonia.Media; using Avalonia.Media.TextFormatting; using Avalonia.Media.TextFormatting.Unicode; @@ -62,7 +63,7 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting Assert.Equal(2, textRun.Length); - var actual = new CharacterBufferRange(textRun).Span.ToString(); + var actual = textRun.Text.ToString(); Assert.Equal("1 ", actual); @@ -144,8 +145,8 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting var expectedGlyphs = expected.TextLines.Select(x => string.Join('|', x.TextRuns.Cast() .SelectMany(x => x.ShapedBuffer.GlyphIndices))).ToList(); - var outer = new GraphemeEnumerator(new CharacterBufferRange(text)); - var inner = new GraphemeEnumerator(new CharacterBufferRange(text)); + var outer = new GraphemeEnumerator(text); + var inner = new GraphemeEnumerator(text); var i = 0; var j = 0; @@ -153,7 +154,7 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting { while (inner.MoveNext()) { - j += inner.Current.Length; + j += inner.Current.Text.Length; if (j + i > text.Length) { @@ -190,9 +191,9 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting break; } - inner = new GraphemeEnumerator(new CharacterBufferRange(text)); + inner = new GraphemeEnumerator(text); - i += outer.Current.Length; + i += outer.Current.Text.Length; } } } @@ -261,7 +262,7 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting Assert.Equal(2, textRun.Length); - var actual = new CharacterBufferRange(textRun).Span.ToString(); + var actual = textRun.Text.ToString(); Assert.Equal("89", actual); @@ -331,7 +332,7 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting Assert.Equal(2, textRun.Length); - var actual = new CharacterBufferRange(textRun).Span.ToString(); + var actual = textRun.Text.ToString(); Assert.Equal("😄", actual); @@ -667,10 +668,10 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting Assert.Equal(5, layout.TextLines.Count); - Assert.Equal("123\r\n", new CharacterBufferRange(layout.TextLines[0].TextRuns[0])); - Assert.Equal("\r\n", new CharacterBufferRange(layout.TextLines[1].TextRuns[0])); - Assert.Equal("456\r\n", new CharacterBufferRange(layout.TextLines[2].TextRuns[0])); - Assert.Equal("\r\n", new CharacterBufferRange(layout.TextLines[3].TextRuns[0])); + Assert.Equal("123\r\n", layout.TextLines[0].TextRuns[0].Text.ToString()); + Assert.Equal("\r\n", layout.TextLines[1].TextRuns[0].Text.ToString()); + Assert.Equal("456\r\n", layout.TextLines[2].TextRuns[0].Text.ToString()); + Assert.Equal("\r\n", layout.TextLines[3].TextRuns[0].Text.ToString()); } } @@ -815,8 +816,9 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting Assert.True(textLine.Width <= maxWidth); var actual = new string(textLine.TextRuns.Cast() - .OrderBy(x => x.CharacterBufferReference.OffsetToFirstChar) - .SelectMany(x => new CharacterBufferRange(x.CharacterBufferReference, x.Length)).ToArray()); + .OrderBy(x => TextTestHelper.GetStartCharIndex(x.Text)) + .SelectMany(x => x.Text.ToString()) + .ToArray()); var expected = text.Substring(textLine.FirstTextSourceIndex, textLine.Length); @@ -968,15 +970,15 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting var i = 0; - var graphemeEnumerator = new GraphemeEnumerator(new CharacterBufferRange(text)); + var graphemeEnumerator = new GraphemeEnumerator(text); while (graphemeEnumerator.MoveNext()) { var grapheme = graphemeEnumerator.Current; - var textStyleOverrides = new[] { new ValueSpan(i, grapheme.Length, new GenericTextRunProperties(Typeface.Default, 12, foregroundBrush: Brushes.Red)) }; + var textStyleOverrides = new[] { new ValueSpan(i, grapheme.Text.Length, new GenericTextRunProperties(Typeface.Default, 12, foregroundBrush: Brushes.Red)) }; - i += grapheme.Length; + i += grapheme.Text.Length; var layout = new TextLayout( text, @@ -1020,6 +1022,8 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting } } + + private static IDisposable Start() { var disposable = UnitTestApplication.Start(TestServices.MockPlatformRenderInterface diff --git a/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextLineTests.cs b/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextLineTests.cs index 2c6ccfa896..6993c70e8b 100644 --- a/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextLineTests.cs +++ b/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextLineTests.cs @@ -2,6 +2,7 @@ using System.Collections.Generic; using System.Globalization; using System.Linq; +using System.Runtime.InteropServices; using Avalonia.Media; using Avalonia.Media.TextFormatting; using Avalonia.UnitTests; @@ -90,7 +91,7 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting var clusters = new List(); - foreach (var textRun in textLine.TextRuns.OrderBy(x => x.CharacterBufferReference.OffsetToFirstChar)) + foreach (var textRun in textLine.TextRuns.OrderBy(x => TextTestHelper.GetStartCharIndex(x.Text))) { var shapedRun = (ShapedTextRun)textRun; @@ -137,7 +138,7 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting var clusters = new List(); - foreach (var textRun in textLine.TextRuns.OrderBy(x => x.CharacterBufferReference.OffsetToFirstChar)) + foreach (var textRun in textLine.TextRuns.OrderBy(x => TextTestHelper.GetStartCharIndex(x.Text))) { var shapedRun = (ShapedTextRun)textRun; @@ -410,7 +411,7 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting Assert.True(collapsedLine.HasCollapsed); - var trimmedText = collapsedLine.TextRuns.SelectMany(x => new CharacterBufferRange(x)).ToArray(); + var trimmedText = collapsedLine.TextRuns.SelectMany(x => x.Text.ToString()).ToArray(); Assert.Equal(expected.Length, trimmedText.Length); @@ -653,7 +654,7 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting var run = textRuns[i]; var bounds = runBounds[i]; - Assert.Equal(run.CharacterBufferReference.OffsetToFirstChar, bounds.TextSourceCharacterIndex); + Assert.Equal(TextTestHelper.GetStartCharIndex(run.Text), bounds.TextSourceCharacterIndex); Assert.Equal(run, bounds.TextRun); Assert.Equal(run.Size.Width, bounds.Rectangle.Width); } diff --git a/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextShaperTests.cs b/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextShaperTests.cs index 63e0083b1d..834dce4a90 100644 --- a/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextShaperTests.cs +++ b/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextShaperTests.cs @@ -18,7 +18,7 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting var options = new TextShaperOptions(Typeface.Default.GlyphTypeface, 12,0, CultureInfo.CurrentCulture); var shapedBuffer = TextShaper.Current.ShapeText(text, options); - Assert.Equal(shapedBuffer.CharacterBufferRange.Length, text.Length); + Assert.Equal(shapedBuffer.Length, text.Length); Assert.Equal(shapedBuffer.GlyphClusters.Count, text.Length); Assert.Equal(0, shapedBuffer.GlyphClusters[0]); Assert.Equal(1, shapedBuffer.GlyphClusters[1]); diff --git a/tests/Avalonia.UnitTests/HarfBuzzTextShaperImpl.cs b/tests/Avalonia.UnitTests/HarfBuzzTextShaperImpl.cs index ae7e00aca1..566cb0f1ac 100644 --- a/tests/Avalonia.UnitTests/HarfBuzzTextShaperImpl.cs +++ b/tests/Avalonia.UnitTests/HarfBuzzTextShaperImpl.cs @@ -1,18 +1,21 @@ using System; +using System.Buffers; using System.Globalization; +using System.Runtime.InteropServices; using Avalonia.Media.TextFormatting; using Avalonia.Media.TextFormatting.Unicode; using Avalonia.Platform; -using Avalonia.Utilities; using HarfBuzzSharp; using Buffer = HarfBuzzSharp.Buffer; +using GlyphInfo = HarfBuzzSharp.GlyphInfo; namespace Avalonia.UnitTests { - public class HarfBuzzTextShaperImpl : ITextShaperImpl + internal class HarfBuzzTextShaperImpl : ITextShaperImpl { - public ShapedBuffer ShapeText(CharacterBufferReference text, int textLength, TextShaperOptions options) + public ShapedBuffer ShapeText(ReadOnlyMemory text, TextShaperOptions options) { + var textSpan = text.Span; var typeface = options.Typeface; var fontRenderingEmSize = options.FontRenderingEmSize; var bidiLevel = options.BidiLevel; @@ -20,15 +23,17 @@ namespace Avalonia.UnitTests using (var buffer = new Buffer()) { - buffer.AddUtf16(text.CharacterBuffer.Span, text.OffsetToFirstChar, textLength); + // HarfBuzz needs the surrounding characters to correctly shape the text + var containingText = GetContainingMemory(text, out var start, out var length); + buffer.AddUtf16(containingText.Span, start, length); MergeBreakPair(buffer); - + buffer.GuessSegmentProperties(); buffer.Direction = (bidiLevel & 1) == 0 ? Direction.LeftToRight : Direction.RightToLeft; - buffer.Language = new Language(culture ?? CultureInfo.CurrentCulture); + buffer.Language = new Language(culture ?? CultureInfo.CurrentCulture); var font = ((HarfBuzzGlyphTypefaceImpl)typeface).Font; @@ -45,9 +50,7 @@ namespace Avalonia.UnitTests var bufferLength = buffer.Length; - var characterBufferRange = new CharacterBufferRange(text, textLength); - - var shapedBuffer = new ShapedBuffer(characterBufferRange, bufferLength, typeface, fontRenderingEmSize, bidiLevel); + var shapedBuffer = new ShapedBuffer(text, bufferLength, typeface, fontRenderingEmSize, bidiLevel); var glyphInfos = buffer.GetGlyphInfoSpan(); @@ -59,12 +62,21 @@ namespace Avalonia.UnitTests var glyphIndex = (ushort)sourceInfo.Codepoint; - var glyphCluster = (int)sourceInfo.Cluster; + var glyphCluster = (int)(sourceInfo.Cluster); - var glyphAdvance = GetGlyphAdvance(glyphPositions, i, textScale); + var glyphAdvance = GetGlyphAdvance(glyphPositions, i, textScale) + options.LetterSpacing; var glyphOffset = GetGlyphOffset(glyphPositions, i, textScale); + if (textSpan[i] == '\t') + { + glyphIndex = typeface.GetGlyph(' '); + + glyphAdvance = options.IncrementalTabWidth > 0 ? + options.IncrementalTabWidth : + 4 * typeface.GetGlyphAdvance(glyphIndex) * textScale; + } + var targetInfo = new Media.TextFormatting.GlyphInfo(glyphIndex, glyphCluster, glyphAdvance, glyphOffset); shapedBuffer[i] = targetInfo; @@ -79,7 +91,7 @@ namespace Avalonia.UnitTests var length = buffer.Length; var glyphInfos = buffer.GetGlyphInfoSpan(); - + var second = glyphInfos[length - 1]; if (!new Codepoint(second.Codepoint).IsBreakChar) @@ -90,19 +102,19 @@ namespace Avalonia.UnitTests if (length > 1 && glyphInfos[length - 2].Codepoint == '\r' && second.Codepoint == '\n') { var first = glyphInfos[length - 2]; - + first.Codepoint = '\u200C'; second.Codepoint = '\u200C'; second.Cluster = first.Cluster; unsafe { - fixed (HarfBuzzSharp.GlyphInfo* p = &glyphInfos[length - 2]) + fixed (GlyphInfo* p = &glyphInfos[length - 2]) { *p = first; } - - fixed (HarfBuzzSharp.GlyphInfo* p = &glyphInfos[length - 1]) + + fixed (GlyphInfo* p = &glyphInfos[length - 1]) { *p = second; } @@ -114,7 +126,7 @@ namespace Avalonia.UnitTests unsafe { - fixed (HarfBuzzSharp.GlyphInfo* p = &glyphInfos[length - 1]) + fixed (GlyphInfo* p = &glyphInfos[length - 1]) { *p = second; } @@ -136,8 +148,29 @@ namespace Avalonia.UnitTests private static double GetGlyphAdvance(ReadOnlySpan glyphPositions, int index, double textScale) { // Depends on direction of layout - // advanceBuffer[index] = buffer.GlyphPositions[index].YAdvance * textScale; + // glyphPositions[index].YAdvance * textScale; return glyphPositions[index].XAdvance * textScale; } + + private static ReadOnlyMemory GetContainingMemory(ReadOnlyMemory memory, out int start, out int length) + { + if (MemoryMarshal.TryGetString(memory, out var containingString, out start, out length)) + { + return containingString.AsMemory(); + } + + if (MemoryMarshal.TryGetArray(memory, out var segment)) + { + return segment.Array.AsMemory(); + } + + if (MemoryMarshal.TryGetMemoryManager(memory, out MemoryManager memoryManager, out start, out length)) + { + return memoryManager.Memory; + } + + // should never happen + throw new InvalidOperationException("Memory not backed by string, array or manager"); + } } } diff --git a/tests/Avalonia.UnitTests/MockGlyphRun.cs b/tests/Avalonia.UnitTests/MockGlyphRun.cs index 24948aff01..f525e4736b 100644 --- a/tests/Avalonia.UnitTests/MockGlyphRun.cs +++ b/tests/Avalonia.UnitTests/MockGlyphRun.cs @@ -1,4 +1,5 @@ -using Avalonia.Platform; +using System.Collections.Generic; +using Avalonia.Platform; namespace Avalonia.UnitTests { @@ -8,5 +9,10 @@ namespace Avalonia.UnitTests { } + + public IReadOnlyList GetIntersections(float lowerBound, float upperBound) + { + return null; + } } } diff --git a/tests/Avalonia.UnitTests/MockTextShaperImpl.cs b/tests/Avalonia.UnitTests/MockTextShaperImpl.cs index 00bcef295a..3218139251 100644 --- a/tests/Avalonia.UnitTests/MockTextShaperImpl.cs +++ b/tests/Avalonia.UnitTests/MockTextShaperImpl.cs @@ -1,4 +1,5 @@ -using Avalonia.Media.TextFormatting; +using System; +using Avalonia.Media.TextFormatting; using Avalonia.Media.TextFormatting.Unicode; using Avalonia.Platform; @@ -6,19 +7,20 @@ namespace Avalonia.UnitTests { public class MockTextShaperImpl : ITextShaperImpl { - public ShapedBuffer ShapeText(CharacterBufferReference text, int length, TextShaperOptions options) + public ShapedBuffer ShapeText(ReadOnlyMemory text, TextShaperOptions options) { var typeface = options.Typeface; var fontRenderingEmSize = options.FontRenderingEmSize; var bidiLevel = options.BidiLevel; - var characterBufferRange = new CharacterBufferRange(text, length); - var shapedBuffer = new ShapedBuffer(characterBufferRange, length, typeface, fontRenderingEmSize, bidiLevel); + var shapedBuffer = new ShapedBuffer(text, text.Length, typeface, fontRenderingEmSize, bidiLevel); + var textSpan = text.Span; + var textStartIndex = TextTestHelper.GetStartCharIndex(text); for (var i = 0; i < shapedBuffer.Length;) { - var glyphCluster = i + text.OffsetToFirstChar; + var glyphCluster = i + textStartIndex; - var codepoint = Codepoint.ReadAt(characterBufferRange, i, out var count); + var codepoint = Codepoint.ReadAt(textSpan, i, out var count); var glyphIndex = typeface.GetGlyph(codepoint); diff --git a/tests/Avalonia.UnitTests/TextTestHelper.cs b/tests/Avalonia.UnitTests/TextTestHelper.cs new file mode 100644 index 0000000000..b572333027 --- /dev/null +++ b/tests/Avalonia.UnitTests/TextTestHelper.cs @@ -0,0 +1,15 @@ +using System; +using System.Runtime.InteropServices; + +namespace Avalonia.UnitTests +{ + public static class TextTestHelper + { + public static int GetStartCharIndex(ReadOnlyMemory text) + { + if (!MemoryMarshal.TryGetString(text, out _, out var start, out _)) + throw new InvalidOperationException("text memory should have been a string"); + return start; + } + } +} diff --git a/tests/TestFiles/Direct2D1/OpacityMask/RenderTansform_Applies_To_Opacity_Mask.expected.png b/tests/TestFiles/Direct2D1/OpacityMask/RenderTransform_Applies_To_Opacity_Mask.expected.png similarity index 100% rename from tests/TestFiles/Direct2D1/OpacityMask/RenderTansform_Applies_To_Opacity_Mask.expected.png rename to tests/TestFiles/Direct2D1/OpacityMask/RenderTransform_Applies_To_Opacity_Mask.expected.png diff --git a/tests/TestFiles/Skia/OpacityMask/RenderTansform_Applies_To_Opacity_Mask.expected.png b/tests/TestFiles/Skia/OpacityMask/RenderTransform_Applies_To_Opacity_Mask.expected.png similarity index 100% rename from tests/TestFiles/Skia/OpacityMask/RenderTansform_Applies_To_Opacity_Mask.expected.png rename to tests/TestFiles/Skia/OpacityMask/RenderTransform_Applies_To_Opacity_Mask.expected.png