From 25d8b86992fd326efbb15cabd9e89a2116a7a96d Mon Sep 17 00:00:00 2001 From: robloo Date: Sat, 3 Dec 2022 10:48:00 -0500 Subject: [PATCH 01/58] Add new IsCheckedChanged event to ToggleButton Old events are now obsoleted --- .../Primitives/ToggleButton.cs | 41 ++++++++++++++++--- 1 file changed, 35 insertions(+), 6 deletions(-) diff --git a/src/Avalonia.Controls/Primitives/ToggleButton.cs b/src/Avalonia.Controls/Primitives/ToggleButton.cs index 148797c53a..93ec17f0ca 100644 --- a/src/Avalonia.Controls/Primitives/ToggleButton.cs +++ b/src/Avalonia.Controls/Primitives/ToggleButton.cs @@ -32,20 +32,37 @@ 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; @@ -62,6 +79,7 @@ namespace Avalonia.Controls.Primitives /// /// Raised when a is checked. /// + [Obsolete("Use IsCheckedChanged instead.")] public event EventHandler? Checked { add => AddHandler(CheckedEvent, value); @@ -71,6 +89,7 @@ namespace Avalonia.Controls.Primitives /// /// Raised when a is unchecked. /// + [Obsolete("Use IsCheckedChanged instead.")] public event EventHandler? Unchecked { add => AddHandler(UncheckedEvent, value); @@ -80,12 +99,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. /// @@ -182,13 +211,13 @@ namespace Avalonia.Controls.Primitives switch (newValue) { case true: - OnChecked(new RoutedEventArgs(CheckedEvent)); + OnChecked(new RoutedEventArgs(IsCheckedChangedEvent)); break; case false: - OnUnchecked(new RoutedEventArgs(UncheckedEvent)); + OnUnchecked(new RoutedEventArgs(IsCheckedChangedEvent)); break; default: - OnIndeterminate(new RoutedEventArgs(IndeterminateEvent)); + OnIndeterminate(new RoutedEventArgs(IsCheckedChangedEvent)); break; } } From 0a36aec64a1f312578b02e5d9dd63ac7e3c39b3a Mon Sep 17 00:00:00 2001 From: robloo Date: Sat, 3 Dec 2022 10:48:24 -0500 Subject: [PATCH 02/58] Fix IsCheckedChanged in ToggleSplitButton to be nullable --- src/Avalonia.Controls/SplitButton/ToggleSplitButton.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Avalonia.Controls/SplitButton/ToggleSplitButton.cs b/src/Avalonia.Controls/SplitButton/ToggleSplitButton.cs index 83bc9851c1..fd7ea28ba1 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); From 59ee997516c1d96f1a7fe4c774bb36e619f7d963 Mon Sep 17 00:00:00 2001 From: robloo Date: Sat, 3 Dec 2022 10:50:56 -0500 Subject: [PATCH 03/58] Remove code section formatting blocks In later commits it was agreed not to include these in the future. --- .../SplitButton/SplitButton.cs | 20 ------------------- .../SplitButton/ToggleSplitButton.cs | 16 --------------- 2 files changed, 36 deletions(-) diff --git a/src/Avalonia.Controls/SplitButton/SplitButton.cs b/src/Avalonia.Controls/SplitButton/SplitButton.cs index f39064435d..b2139f01e9 100644 --- a/src/Avalonia.Controls/SplitButton/SplitButton.cs +++ b/src/Avalonia.Controls/SplitButton/SplitButton.cs @@ -68,10 +68,6 @@ namespace Avalonia.Controls private IDisposable? _flyoutPropertyChangedDisposable; - //////////////////////////////////////////////////////////////////////// - // Constructor / Destructors - //////////////////////////////////////////////////////////////////////// - /// /// Initializes a new instance of the class. /// @@ -79,10 +75,6 @@ namespace Avalonia.Controls { } - //////////////////////////////////////////////////////////////////////// - // Properties - //////////////////////////////////////////////////////////////////////// - /// /// Gets or sets the invoked when the primary part is pressed. /// @@ -123,10 +115,6 @@ namespace Avalonia.Controls /// protected override bool IsEnabledCore => base.IsEnabledCore && _commandCanExecute; - //////////////////////////////////////////////////////////////////////// - // Methods - //////////////////////////////////////////////////////////////////////// - /// void ICommandSource.CanExecuteChanged(object sender, EventArgs e) => this.CanExecuteChanged(sender, e); @@ -221,10 +209,6 @@ namespace Avalonia.Controls } } - //////////////////////////////////////////////////////////////////////// - // OnEvent Overridable Methods - //////////////////////////////////////////////////////////////////////// - /// protected override void OnApplyTemplate(TemplateAppliedEventArgs e) { @@ -428,10 +412,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 fd7ea28ba1..70d2983b9b 100644 --- a/src/Avalonia.Controls/SplitButton/ToggleSplitButton.cs +++ b/src/Avalonia.Controls/SplitButton/ToggleSplitButton.cs @@ -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) { From f4827bdfdbe22acb37132029975ed0fd711e8a65 Mon Sep 17 00:00:00 2001 From: robloo Date: Sat, 3 Dec 2022 10:52:29 -0500 Subject: [PATCH 04/58] Fix Click in SplitButton to be nullable --- src/Avalonia.Controls/SplitButton/SplitButton.cs | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/Avalonia.Controls/SplitButton/SplitButton.cs b/src/Avalonia.Controls/SplitButton/SplitButton.cs index b2139f01e9..353674b26e 100644 --- a/src/Avalonia.Controls/SplitButton/SplitButton.cs +++ b/src/Avalonia.Controls/SplitButton/SplitButton.cs @@ -24,7 +24,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); @@ -34,7 +34,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. From 0bb1b2ec03c7caf68d9a9fdcfa77d11ed51ce518 Mon Sep 17 00:00:00 2001 From: robloo Date: Sat, 3 Dec 2022 11:37:45 -0500 Subject: [PATCH 05/58] Fix ToggleButton tests --- .../Primitives/ToggleButton.cs | 47 ++++++++++++++----- 1 file changed, 34 insertions(+), 13 deletions(-) diff --git a/src/Avalonia.Controls/Primitives/ToggleButton.cs b/src/Avalonia.Controls/Primitives/ToggleButton.cs index 93ec17f0ca..dfb436a55e 100644 --- a/src/Avalonia.Controls/Primitives/ToggleButton.cs +++ b/src/Avalonia.Controls/Primitives/ToggleButton.cs @@ -68,7 +68,6 @@ namespace Avalonia.Controls.Primitives static ToggleButton() { - IsCheckedProperty.Changed.AddClassHandler((x, e) => x.OnIsCheckedChanged(e)); } public ToggleButton() @@ -176,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); @@ -185,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); @@ -194,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(IsCheckedChangedEvent)); - break; - case false: - OnUnchecked(new RoutedEventArgs(IsCheckedChangedEvent)); - break; - default: - OnIndeterminate(new RoutedEventArgs(IsCheckedChangedEvent)); - 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)); } } From 7eb95255cb1055c7d981b6ff20b86a014c9dc570 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Thu, 15 Dec 2022 14:16:04 +0100 Subject: [PATCH 06/58] Don't mark all owned windows as dialogs. This was preventing owner windows of non-modal dialogs from receiving key window status, meaning that input was broken in combination with embedding (Avalonia controls did work however because the routing was carried out internal to Avalonia). --- native/Avalonia.Native/src/OSX/WindowImpl.mm | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/native/Avalonia.Native/src/OSX/WindowImpl.mm b/native/Avalonia.Native/src/OSX/WindowImpl.mm index b6dacb6ce4..bbf419bccd 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; + _isDialog = isDialog; WindowBaseImpl::Show(activate, isDialog); From 102b1b351bce7a75be28c51ac70e19d50f786126 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Thu, 15 Dec 2022 14:17:19 +0100 Subject: [PATCH 07/58] Add null check, Customer was seeing a crash in this method, don't have a repro but other uses of `_parent` are surrounded by a null check so this makes sense I think. --- native/Avalonia.Native/src/OSX/AvnWindow.mm | 3 +++ 1 file changed, 3 insertions(+) diff --git a/native/Avalonia.Native/src/OSX/AvnWindow.mm b/native/Avalonia.Native/src/OSX/AvnWindow.mm index 6611dcc73f..42c500716c 100644 --- a/native/Avalonia.Native/src/OSX/AvnWindow.mm +++ b/native/Avalonia.Native/src/OSX/AvnWindow.mm @@ -281,6 +281,9 @@ - (void)windowDidBecomeKey:(NSNotification *_Nonnull)notification { + if (_parent == nullptr) + return; + _parent->BringToFront(); dispatch_async(dispatch_get_main_queue(), ^{ From 872eca05b2df322077c273be1922201381520e12 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Fri, 16 Dec 2022 09:01:15 +0100 Subject: [PATCH 08/58] Disable minimize button on owned windows again. 7eb95255c regressed disabling the minimize button on owned windows. Split what was previously `IsDialog` into two flags: `IsModal` and `IsOwned` and use `IsOwned` to control the disabled state of the minimize button. --- native/Avalonia.Native/src/OSX/AvnWindow.mm | 2 +- .../Avalonia.Native/src/OSX/WindowBaseImpl.h | 2 +- .../Avalonia.Native/src/OSX/WindowBaseImpl.mm | 2 +- native/Avalonia.Native/src/OSX/WindowImpl.h | 6 ++++-- native/Avalonia.Native/src/OSX/WindowImpl.mm | 18 +++++++++++------- 5 files changed, 18 insertions(+), 12 deletions(-) diff --git a/native/Avalonia.Native/src/OSX/AvnWindow.mm b/native/Avalonia.Native/src/OSX/AvnWindow.mm index 42c500716c..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 diff --git a/native/Avalonia.Native/src/OSX/WindowBaseImpl.h b/native/Avalonia.Native/src/OSX/WindowBaseImpl.h index 7fb002e54f..bb8d5fae75 100644 --- a/native/Avalonia.Native/src/OSX/WindowBaseImpl.h +++ b/native/Avalonia.Native/src/OSX/WindowBaseImpl.h @@ -96,7 +96,7 @@ BEGIN_INTERFACE_MAP() 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..9650290fbb 100644 --- a/native/Avalonia.Native/src/OSX/WindowBaseImpl.mm +++ b/native/Avalonia.Native/src/OSX/WindowBaseImpl.mm @@ -542,7 +542,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 bbf419bccd..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; + _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; } From 05d8b84ce3e4f30c6c5c643ce6cbf841c24121d5 Mon Sep 17 00:00:00 2001 From: rabbitism Date: Tue, 3 Jan 2023 23:21:53 +0800 Subject: [PATCH 09/58] feat: Add GeometryConverter --- src/Avalonia.Base/Media/Geometry.cs | 33 ++++++++++++++++++++++++++++- 1 file changed, 32 insertions(+), 1 deletion(-) diff --git a/src/Avalonia.Base/Media/Geometry.cs b/src/Avalonia.Base/Media/Geometry.cs index 2019f54c70..b1fea53289 100644 --- a/src/Avalonia.Base/Media/Geometry.cs +++ b/src/Avalonia.Base/Media/Geometry.cs @@ -1,11 +1,14 @@ using System; using Avalonia.Platform; +using System.ComponentModel; +using System.Globalization; namespace Avalonia.Media { /// /// Defines a geometric shape. - /// + /// + [TypeConverter(typeof(GeometryConverter))] public abstract class Geometry : AvaloniaObject { /// @@ -199,4 +202,32 @@ namespace Avalonia.Media return new CombinedGeometry(combineMode, geometry1, geometry2, transform); } } + + public class GeometryConverter : 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); + } + } } From 6e1311c75d16547920b05812fcffa225da2d2893 Mon Sep 17 00:00:00 2001 From: rabbitism Date: Wed, 4 Jan 2023 00:09:51 +0800 Subject: [PATCH 10/58] fix: fix nullable issue. --- src/Avalonia.Base/Media/Geometry.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Avalonia.Base/Media/Geometry.cs b/src/Avalonia.Base/Media/Geometry.cs index b1fea53289..e2b345df1a 100644 --- a/src/Avalonia.Base/Media/Geometry.cs +++ b/src/Avalonia.Base/Media/Geometry.cs @@ -214,13 +214,13 @@ namespace Avalonia.Media return base.CanConvertFrom(context, sourceType); } - public override object ConvertFrom(ITypeDescriptorContext? context, CultureInfo? culture, object value) + public override object? ConvertFrom(ITypeDescriptorContext? context, CultureInfo? culture, object value) { if (value is null) { throw GetConvertFromException(value); } - string source = value as string; + string? source = value as string; if (source != null) { From dba476c11c0f3c92be8b3356c6a02ea393c52cc2 Mon Sep 17 00:00:00 2001 From: Simon Cropp Date: Thu, 5 Jan 2023 22:19:57 +1100 Subject: [PATCH 11/58] fix more typos --- samples/ControlCatalog/Models/StateData.cs | 6 +++--- src/Avalonia.FreeDesktop/DBusMenuExporter.cs | 4 ++-- .../AvaloniaXamlIlSelectorTransformer.cs | 6 +++--- .../AvaloniaObjectTests_SetValue.cs | 8 ++++---- .../Core/ExpressionObserverTests_DataValidation.cs | 2 +- .../Core/ExpressionObserverTests_ExpressionTree.cs | 2 +- tests/Avalonia.Base.UnitTests/Input/GesturesTests.cs | 2 +- tests/Avalonia.Base.UnitTests/VisualTests.cs | 4 ++-- tests/Avalonia.Controls.UnitTests/WindowTests.cs | 12 ++++++------ .../Xaml/BasicTests.cs | 4 ++-- .../Xaml/ControlTemplateTests.cs | 6 +++--- .../Xaml/StyleTests.cs | 4 ++-- .../Avalonia.RenderTests/Controls/TextBlockTests.cs | 4 ++-- .../Media/TextFormatting/TextLayoutTests.cs | 2 +- tests/Avalonia.RenderTests/OpacityMaskTests.cs | 2 +- 15 files changed, 34 insertions(+), 34 deletions(-) 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/src/Avalonia.FreeDesktop/DBusMenuExporter.cs b/src/Avalonia.FreeDesktop/DBusMenuExporter.cs index 657e324010..4466b938cf 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/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/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/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.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.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 = @" 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 70a5295008..ae1b12572e 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 { From 209861d9d405999c0e485a85637a61cfb2c1cb64 Mon Sep 17 00:00:00 2001 From: Simon Cropp Date: Fri, 6 Jan 2023 10:31:08 +1100 Subject: [PATCH 12/58] . --- ...rTransform_Applies_To_Opacity_Mask.expected.png} | Bin ...rTransform_Applies_To_Opacity_Mask.expected.png} | Bin 2 files changed, 0 insertions(+), 0 deletions(-) rename tests/TestFiles/Direct2D1/OpacityMask/{RenderTansform_Applies_To_Opacity_Mask.expected.png => RenderTransform_Applies_To_Opacity_Mask.expected.png} (100%) rename tests/TestFiles/Skia/OpacityMask/{RenderTansform_Applies_To_Opacity_Mask.expected.png => RenderTransform_Applies_To_Opacity_Mask.expected.png} (100%) 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 From d53ce9a0e9e2004d447df81311d862e90c96ec98 Mon Sep 17 00:00:00 2001 From: Simon Cropp Date: Fri, 6 Jan 2023 11:04:20 +1100 Subject: [PATCH 13/58] Update BrowserStorageProvider.cs --- .../Storage/BrowserStorageProvider.cs | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) 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); } } From 54c96bc89469708dfbf0687d6dc6e2f9bd382464 Mon Sep 17 00:00:00 2001 From: Simon Cropp Date: Fri, 6 Jan 2023 11:05:31 +1100 Subject: [PATCH 14/58] Update IOSStorageProvider.cs --- src/iOS/Avalonia.iOS/Storage/IOSStorageProvider.cs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) 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) From fd7c89b611f3cb18d92cf06d726689fd00eaf804 Mon Sep 17 00:00:00 2001 From: Max Katz Date: Fri, 6 Jan 2023 02:01:55 -0500 Subject: [PATCH 15/58] Initial API design for the PlatformColorValues --- .../Platform/DefaultPlatformSettings.cs | 24 ++++++++--- .../Platform/IPlatformSettings.cs | 15 ++++++- .../Platform/PlatformColorValues.cs | 42 +++++++++++++++++++ 3 files changed, 74 insertions(+), 7 deletions(-) create mode 100644 src/Avalonia.Base/Platform/PlatformColorValues.cs diff --git a/src/Avalonia.Base/Platform/DefaultPlatformSettings.cs b/src/Avalonia.Base/Platform/DefaultPlatformSettings.cs index d0b27b057f..98f2367819 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,20 @@ 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(PlatformThemeVariant.Light); + } + + public event EventHandler? ColorValuesChanged; + + protected void OnColorValuesChanged(PlatformColorValues colorValues) + { + ColorValuesChanged?.Invoke(this, colorValues); + } } } 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/PlatformColorValues.cs b/src/Avalonia.Base/Platform/PlatformColorValues.cs new file mode 100644 index 0000000000..3071cc217f --- /dev/null +++ b/src/Avalonia.Base/Platform/PlatformColorValues.cs @@ -0,0 +1,42 @@ +using Avalonia.Media; + +namespace Avalonia.Platform; + +/// +/// System theme variant or mode. +/// +public enum PlatformThemeVariant +{ + Light, + Dark +} + +/// +/// Information about current system color values, including information about dark mode and accent colors. +/// +/// System theme variant or mode. +/// Primary system accent color. +/// Secondary system accent color. On some platforms can return the same value as AccentColor1. +/// Tertiary system accent color. On some platforms can return the same value as AccentColor1. +public record struct PlatformColorValues( + PlatformThemeVariant ThemeVariant, + Color AccentColor1, + Color AccentColor2, + Color AccentColor3) +{ + public PlatformColorValues( + PlatformThemeVariant ThemeVariant, + Color AccentColor1) + : this(ThemeVariant, AccentColor1, AccentColor1, AccentColor1) + { + + } + + public PlatformColorValues(PlatformThemeVariant ThemeVariant) + : this(ThemeVariant, DefaultAccent) + { + + } + + private static Color DefaultAccent => new(255, 0, 120, 215); +} From ad4005ef05ba4b4a6fa0679ea0e4132119c12216 Mon Sep 17 00:00:00 2001 From: Max Katz Date: Fri, 6 Jan 2023 02:02:12 -0500 Subject: [PATCH 16/58] Use PlatformColorValues in the control catalog --- samples/ControlCatalog/MainView.xaml.cs | 73 ++++++++++++++++++++++++- 1 file changed, 72 insertions(+), 1 deletion(-) diff --git a/samples/ControlCatalog/MainView.xaml.cs b/samples/ControlCatalog/MainView.xaml.cs index f3c1a68e72..ecd9d0bf52 100644 --- a/samples/ControlCatalog/MainView.xaml.cs +++ b/samples/ControlCatalog/MainView.xaml.cs @@ -3,9 +3,12 @@ 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.Styling; using Avalonia.VisualTree; using ControlCatalog.Models; using ControlCatalog.Pages; @@ -14,10 +17,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 +44,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 +105,61 @@ namespace ControlCatalog var decorations = this.Get("Decorations"); if (VisualRoot is Window window) decorations.SelectedIndex = (int)window.SystemDecorations; + + _platformSettings.ColorValuesChanged += PlatformSettingsOnColorValuesChanged; + } + + 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); + } } } } From 36f93ae7bc71fd2f54cc36b78706546e0d9ca501 Mon Sep 17 00:00:00 2001 From: Max Katz Date: Fri, 6 Jan 2023 02:14:16 -0500 Subject: [PATCH 17/58] Update tests and designer --- .../Offscreen/OffscreenTopLevelImpl.cs | 2 + .../Platform/ITopLevelImpl.cs | 6 + src/Avalonia.DesignerSupport/Remote/Stubs.cs | 4 +- src/Avalonia.Headless/HeadlessWindowImpl.cs | 5 + .../Rendering/CompositorTestsBase.cs | 2 + .../HtmlTransport/webapp/package-lock.json | 2665 ++++++++++++++++- 6 files changed, 2682 insertions(+), 2 deletions(-) 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.DesignerSupport/Remote/Stubs.cs b/src/Avalonia.DesignerSupport/Remote/Stubs.cs index 94679e8ade..4bb98c0267 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.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/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.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", From 4105bb09c95491c510b1c0e050480d00e3daf87f Mon Sep 17 00:00:00 2001 From: Max Katz Date: Fri, 6 Jan 2023 02:14:45 -0500 Subject: [PATCH 18/58] Windows implementation --- .../Wpf/WpfTopLevelImpl.cs | 2 + src/Windows/Avalonia.Win32/Win32Platform.cs | 29 ++------- .../Avalonia.Win32/Win32PlatformSettings.cs | 62 +++++++++++++++++++ .../Avalonia.Win32/WinRT/WinRTColor.cs | 3 + src/Windows/Avalonia.Win32/WinRT/winrt.idl | 20 ++++++ .../Avalonia.Win32/WindowImpl.AppWndProc.cs | 8 +++ src/Windows/Avalonia.Win32/WindowImpl.cs | 13 ++++ 7 files changed, 113 insertions(+), 24 deletions(-) create mode 100644 src/Windows/Avalonia.Win32/Win32PlatformSettings.cs 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/Win32Platform.cs b/src/Windows/Avalonia.Win32/Win32Platform.cs index 585a9cdf19..c0a0ce4da0 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; @@ -403,25 +404,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..73ac5bf5a9 --- /dev/null +++ b/src/Windows/Avalonia.Win32/Win32PlatformSettings.cs @@ -0,0 +1,62 @@ +using System; +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 settings = NativeWinRTMethods.CreateInstance("Windows.UI.ViewManagement.UISettings"); + var accent = settings.GetColorValue(UIColorType.Accent).ToAvalonia(); + var background = settings.GetColorValue(UIColorType.Background).ToAvalonia(); + + return _lastColorValues = new PlatformColorValues( + background.R + background.G + background.B < (255 * 3 - background.R - background.G - background.B) + ? PlatformThemeVariant.Dark + : PlatformThemeVariant.Light, + accent, accent, accent); + } + + internal void OnColorValuesChanged(IntPtr handle) + { + var oldColorValues = _lastColorValues; + var colorValues = GetColorValues(); + + if (oldColorValues != colorValues) + { + OnColorValuesChanged(colorValues); + } + } +} 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..8dee275d50 100644 --- a/src/Windows/Avalonia.Win32/WinRT/winrt.idl +++ b/src/Windows/Avalonia.Win32/WinRT/winrt.idl @@ -844,3 +844,23 @@ 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); +} diff --git a/src/Windows/Avalonia.Win32/WindowImpl.AppWndProc.cs b/src/Windows/Avalonia.Win32/WindowImpl.AppWndProc.cs index 8c362b0c29..af634fd760 100644 --- a/src/Windows/Avalonia.Win32/WindowImpl.AppWndProc.cs +++ b/src/Windows/Avalonia.Win32/WindowImpl.AppWndProc.cs @@ -732,6 +732,14 @@ namespace Avalonia.Win32 var node = AutomationNode.GetOrCreate(peer); return UiaCoreProviderApi.UiaReturnRawElementProvider(_hwnd, wParam, lParam, node); } + break; + case WindowsMessage.WM_SETTINGCHANGE: + if (Marshal.PtrToStringAuto(lParam) == "ImmersiveColorSet" + && Win32Platform.PlatformSettings is Win32PlatformSettings win32PlatformSettings) + { + win32PlatformSettings.OnColorValuesChanged(_hwnd); + } + break; } 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( From d76bdd7807f9612793708ac50033b2e806d53cd1 Mon Sep 17 00:00:00 2001 From: Max Katz Date: Fri, 6 Jan 2023 02:15:00 -0500 Subject: [PATCH 19/58] FreeDesktop/x11 implementation --- .../DBusPlatformSettings.cs | 74 +++++++++++++++++++ src/Avalonia.FreeDesktop/DBusSettings.cs | 16 ++++ src/Avalonia.X11/X11Platform.cs | 2 +- src/Avalonia.X11/X11Window.cs | 2 + .../FramebufferToplevelImpl.cs | 2 + 5 files changed, 95 insertions(+), 1 deletion(-) create mode 100644 src/Avalonia.FreeDesktop/DBusPlatformSettings.cs create mode 100644 src/Avalonia.FreeDesktop/DBusSettings.cs diff --git a/src/Avalonia.FreeDesktop/DBusPlatformSettings.cs b/src/Avalonia.FreeDesktop/DBusPlatformSettings.cs new file mode 100644 index 0000000000..82b4a72e74 --- /dev/null +++ b/src/Avalonia.FreeDesktop/DBusPlatformSettings.cs @@ -0,0 +1,74 @@ +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.Value); + } + 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.Value); + } + } + + private static PlatformColorValues GetColorValuesFromSetting(object value) + { + var isDark = value?.ToString() == "1"; + return new PlatformColorValues(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.X11/X11Platform.cs b/src/Avalonia.X11/X11Platform.cs index 9692e0a384..7820f8a3c2 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 810a806c8a..4b0e8473cc 100644 --- a/src/Avalonia.X11/X11Window.cs +++ b/src/Avalonia.X11/X11Window.cs @@ -1225,6 +1225,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/Linux/Avalonia.LinuxFramebuffer/FramebufferToplevelImpl.cs b/src/Linux/Avalonia.LinuxFramebuffer/FramebufferToplevelImpl.cs index ac54365f51..1c2fad56a6 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); } } From a4bffbf660595ea70e3073401fd3df43746fd73f Mon Sep 17 00:00:00 2001 From: Max Katz Date: Fri, 6 Jan 2023 02:15:18 -0500 Subject: [PATCH 20/58] iOS and Android implementations --- .../Avalonia.Android/AndroidPlatform.cs | 2 +- src/Android/Avalonia.Android/AvaloniaView.cs | 16 ++++ .../Platform/AndroidPlatformSettings.cs | 73 +++++++++++++++++++ .../Platform/SkiaPlatform/TopLevelImpl.cs | 6 ++ src/iOS/Avalonia.iOS/AvaloniaView.cs | 27 ++++++- .../Avalonia.iOS/AvaloniaViewController.cs | 6 ++ src/iOS/Avalonia.iOS/Platform.cs | 2 +- src/iOS/Avalonia.iOS/PlatformSettings.cs | 34 +++++++++ 8 files changed, 163 insertions(+), 3 deletions(-) create mode 100644 src/Android/Avalonia.Android/Platform/AndroidPlatformSettings.cs create mode 100644 src/iOS/Avalonia.iOS/AvaloniaViewController.cs create mode 100644 src/iOS/Avalonia.iOS/PlatformSettings.cs 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/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/Platform/AndroidPlatformSettings.cs b/src/Android/Avalonia.Android/Platform/AndroidPlatformSettings.cs new file mode 100644 index 0000000000..f560dea3ce --- /dev/null +++ b/src/Android/Avalonia.Android/Platform/AndroidPlatformSettings.cs @@ -0,0 +1,73 @@ +using System; +using Android; +using Android.Content; +using Android.Content.Res; +using Android.Graphics; +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( + systemTheme, + new Color(accent1.A, accent1.R, accent1.G, accent1.B), + new Color(accent2.A, accent2.R, accent2.G, accent2.B), + 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( + systemTheme, + new Color(accent.A, accent.R, accent.G, accent.B)); + array.Recycle(); + } + else + { + _latestValues = _latestValues with { ThemeVariant = systemTheme }; + } + + OnColorValuesChanged(_latestValues); + } +} diff --git a/src/Android/Avalonia.Android/Platform/SkiaPlatform/TopLevelImpl.cs b/src/Android/Avalonia.Android/Platform/SkiaPlatform/TopLevelImpl.cs index 1aac567dda..1f997af26d 100644 --- a/src/Android/Avalonia.Android/Platform/SkiaPlatform/TopLevelImpl.cs +++ b/src/Android/Avalonia.Android/Platform/SkiaPlatform/TopLevelImpl.cs @@ -26,6 +26,7 @@ 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 @@ -286,6 +287,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; diff --git a/src/iOS/Avalonia.iOS/AvaloniaView.cs b/src/iOS/Avalonia.iOS/AvaloniaView.cs index 48358c745f..6d36ca7814 100644 --- a/src/iOS/Avalonia.iOS/AvaloniaView.cs +++ b/src/iOS/Avalonia.iOS/AvaloniaView.cs @@ -52,6 +52,14 @@ 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(); + } + internal class TopLevelImpl : ITopLevelImplWithTextInputMethod, ITopLevelImplWithNativeControlHost, ITopLevelImplWithStorageProvider { @@ -120,7 +128,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/AvaloniaViewController.cs b/src/iOS/Avalonia.iOS/AvaloniaViewController.cs new file mode 100644 index 0000000000..c6856789b8 --- /dev/null +++ b/src/iOS/Avalonia.iOS/AvaloniaViewController.cs @@ -0,0 +1,6 @@ +namespace Avalonia.iOS; + +public class AvaloniaViewController +{ + +} 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..736e94ffc6 --- /dev/null +++ b/src/iOS/Avalonia.iOS/PlatformSettings.cs @@ -0,0 +1,34 @@ +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; + + return _lastColorValues = new PlatformColorValues(themeVariant); + } + + public void TraitCollectionDidChange() + { + var oldColorValues = _lastColorValues; + var colorValues = GetColorValues(); + + if (oldColorValues != colorValues) + { + OnColorValuesChanged(colorValues); + } + } +} From 70dcf71910afa309d6ec5ae35729e501a3073ec7 Mon Sep 17 00:00:00 2001 From: Max Katz Date: Fri, 6 Jan 2023 02:15:26 -0500 Subject: [PATCH 21/58] Browser implementation --- .../BrowserPlatformSettings.cs | 26 +++++++++++++++++++ .../Avalonia.Browser/BrowserTopLevelImpl.cs | 5 ++++ .../Avalonia.Browser/Interop/DomHelper.cs | 5 ++++ .../Avalonia.Browser/WindowingPlatform.cs | 2 +- .../webapp/modules/avalonia/dom.ts | 13 ++++++++++ 5 files changed, 50 insertions(+), 1 deletion(-) create mode 100644 src/Browser/Avalonia.Browser/BrowserPlatformSettings.cs diff --git a/src/Browser/Avalonia.Browser/BrowserPlatformSettings.cs b/src/Browser/Avalonia.Browser/BrowserPlatformSettings.cs new file mode 100644 index 0000000000..8a5a089752 --- /dev/null +++ b/src/Browser/Avalonia.Browser/BrowserPlatformSettings.cs @@ -0,0 +1,26 @@ +using Avalonia.Browser.Interop; +using Avalonia.Platform; + +namespace Avalonia.Browser; + +internal class BrowserPlatformSettings : DefaultPlatformSettings +{ + private bool _isDarkMode; + + public BrowserPlatformSettings() + { + _isDarkMode = DomHelper.ObserveDarkMode(m => + { + _isDarkMode = m; + OnColorValuesChanged(GetColorValues()); + }); + } + + public override PlatformColorValues GetColorValues() + { + return base.GetColorValues() with + { + ThemeVariant = _isDarkMode ? PlatformThemeVariant.Dark : PlatformThemeVariant.Light + }; + } +} diff --git a/src/Browser/Avalonia.Browser/BrowserTopLevelImpl.cs b/src/Browser/Avalonia.Browser/BrowserTopLevelImpl.cs index 4e6be3268b..a1f293757c 100644 --- a/src/Browser/Avalonia.Browser/BrowserTopLevelImpl.cs +++ b/src/Browser/Avalonia.Browser/BrowserTopLevelImpl.cs @@ -228,6 +228,11 @@ 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; diff --git a/src/Browser/Avalonia.Browser/Interop/DomHelper.cs b/src/Browser/Avalonia.Browser/Interop/DomHelper.cs index b97a03209b..090f03c4f1 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 bool ObserveDarkMode( + [JSMarshalAs>] + Action onDpiChanged); } 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/dom.ts b/src/Browser/Avalonia.Browser/webapp/modules/avalonia/dom.ts index e8e37faf58..9760fccc3b 100644 --- a/src/Browser/Avalonia.Browser/webapp/modules/avalonia/dom.ts +++ b/src/Browser/Avalonia.Browser/webapp/modules/avalonia/dom.ts @@ -3,6 +3,19 @@ export class AvaloniaDOM { element.classList.add(className); } + static observeDarkMode(observer: (isDarkMode: boolean) => boolean): boolean { + if (globalThis.matchMedia === undefined) { + return false; + } + + const media = globalThis.matchMedia("(prefers-color-scheme: dark)"); + media.addEventListener("change", (args: MediaQueryListEvent) => { + observer(args.matches); + }); + + return media.matches; + } + static createAvaloniaHost(host: HTMLElement) { const randomIdPart = Math.random().toString(36).replace(/[^a-z]+/g, "").substr(2, 10); From 8f11c5a6ede15080385e8fd51c2cfdc5e356ea0a Mon Sep 17 00:00:00 2001 From: Max Katz Date: Fri, 6 Jan 2023 02:15:36 -0500 Subject: [PATCH 22/58] macOS implementation --- .../project.pbxproj | 4 + .../src/OSX/PlatformSettings.mm | 111 ++++++++++++++++++ .../Avalonia.Native/src/OSX/WindowBaseImpl.h | 2 + .../Avalonia.Native/src/OSX/WindowBaseImpl.mm | 18 +++ native/Avalonia.Native/src/OSX/common.h | 1 + native/Avalonia.Native/src/OSX/main.mm | 10 ++ src/Avalonia.Native/AvaloniaNativePlatform.cs | 2 +- src/Avalonia.Native/NativePlatformSettings.cs | 61 ++++++++++ src/Avalonia.Native/WindowImplBase.cs | 5 + src/Avalonia.Native/avn.idl | 16 +++ 10 files changed, 229 insertions(+), 1 deletion(-) create mode 100644 native/Avalonia.Native/src/OSX/PlatformSettings.mm create mode 100644 src/Avalonia.Native/NativePlatformSettings.cs 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/PlatformSettings.mm b/native/Avalonia.Native/src/OSX/PlatformSettings.mm new file mode 100644 index 0000000000..e709cd5135 --- /dev/null +++ b/native/Avalonia.Native/src/OSX/PlatformSettings.mm @@ -0,0 +1,111 @@ +#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 + || NSApplication.sharedApplication.effectiveAppearance.name == NSAppearanceNameAccessibilityHighContrastAqua + || NSApplication.sharedApplication.effectiveAppearance.name == NSAppearanceNameAccessibilityHighContrastVibrantLight) { + return AvnPlatformThemeVariant::Light; + } else if (NSApplication.sharedApplication.effectiveAppearance.name == NSAppearanceNameDarkAqua + || NSApplication.sharedApplication.effectiveAppearance.name == NSAppearanceNameVibrantDark + || NSApplication.sharedApplication.effectiveAppearance.name == NSAppearanceNameAccessibilityHighContrastDarkAqua + || NSApplication.sharedApplication.effectiveAppearance.name == NSAppearanceNameAccessibilityHighContrastVibrantDark) { + return AvnPlatformThemeVariant::Dark; + } + } + 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..1404ec361c 100644 --- a/native/Avalonia.Native/src/OSX/WindowBaseImpl.h +++ b/native/Avalonia.Native/src/OSX/WindowBaseImpl.h @@ -92,6 +92,8 @@ 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; diff --git a/native/Avalonia.Native/src/OSX/WindowBaseImpl.mm b/native/Avalonia.Native/src/OSX/WindowBaseImpl.mm index 9946ad9b10..39dde5a582 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; 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/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/NativePlatformSettings.cs b/src/Avalonia.Native/NativePlatformSettings.cs new file mode 100644 index 0000000000..cea748bbae --- /dev/null +++ b/src/Avalonia.Native/NativePlatformSettings.cs @@ -0,0 +1,61 @@ +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 = (PlatformThemeVariant)_platformSettings.PlatformTheme; + var color = _platformSettings.AccentColor; + + if (color > 0) + { + _lastColorValues = new PlatformColorValues(theme, Color.FromUInt32(color)); + } + else + { + _lastColorValues = new PlatformColorValues(theme); + } + + 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..29f839c826 100644 --- a/src/Avalonia.Native/avn.idl +++ b/src/Avalonia.Native/avn.idl @@ -473,6 +473,12 @@ enum AvnWindowTransparencyMode Blur } +enum AvnPlatformThemeVariant +{ + Light, + Dark +} + [uuid(809c652e-7396-11d2-9771-00a0c9b4d50c)] interface IAvaloniaNativeFactory : IUnknown { @@ -494,6 +500,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 +542,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 +914,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); +} From f01ea6a8a11c8750cc5c72a9be5a0867709834a5 Mon Sep 17 00:00:00 2001 From: Max Katz Date: Fri, 6 Jan 2023 02:26:41 -0500 Subject: [PATCH 23/58] Fix control catalog on mobile --- samples/ControlCatalog/MainView.xaml.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/samples/ControlCatalog/MainView.xaml.cs b/samples/ControlCatalog/MainView.xaml.cs index ecd9d0bf52..8c8be2638e 100644 --- a/samples/ControlCatalog/MainView.xaml.cs +++ b/samples/ControlCatalog/MainView.xaml.cs @@ -107,6 +107,7 @@ namespace ControlCatalog decorations.SelectedIndex = (int)window.SystemDecorations; _platformSettings.ColorValuesChanged += PlatformSettingsOnColorValuesChanged; + PlatformSettingsOnColorValuesChanged(_platformSettings, _platformSettings.GetColorValues()); } protected override void OnDetachedFromLogicalTree(LogicalTreeAttachmentEventArgs e) From 148b4a9aca16754a6b22835d6b3983fb060fa92a Mon Sep 17 00:00:00 2001 From: Max Katz Date: Fri, 6 Jan 2023 17:11:50 -0500 Subject: [PATCH 24/58] Changes after review --- samples/ControlCatalog/MainView.xaml.cs | 1 - src/iOS/Avalonia.iOS/AvaloniaViewController.cs | 6 ------ 2 files changed, 7 deletions(-) delete mode 100644 src/iOS/Avalonia.iOS/AvaloniaViewController.cs diff --git a/samples/ControlCatalog/MainView.xaml.cs b/samples/ControlCatalog/MainView.xaml.cs index 8c8be2638e..1726028a3f 100644 --- a/samples/ControlCatalog/MainView.xaml.cs +++ b/samples/ControlCatalog/MainView.xaml.cs @@ -8,7 +8,6 @@ using Avalonia.Markup.Xaml; using Avalonia.Media; using Avalonia.Media.Immutable; using Avalonia.Platform; -using Avalonia.Styling; using Avalonia.VisualTree; using ControlCatalog.Models; using ControlCatalog.Pages; diff --git a/src/iOS/Avalonia.iOS/AvaloniaViewController.cs b/src/iOS/Avalonia.iOS/AvaloniaViewController.cs deleted file mode 100644 index c6856789b8..0000000000 --- a/src/iOS/Avalonia.iOS/AvaloniaViewController.cs +++ /dev/null @@ -1,6 +0,0 @@ -namespace Avalonia.iOS; - -public class AvaloniaViewController -{ - -} From 13c14e7360d032168b9a04f4fdd3089f800808bb Mon Sep 17 00:00:00 2001 From: Max Katz Date: Fri, 6 Jan 2023 22:40:23 -0500 Subject: [PATCH 25/58] Add ColorContrastPreference to the PlatformColorValues --- .../src/OSX/PlatformSettings.mm | 14 ++-- .../Platform/AndroidPlatformSettings.cs | 37 ++++++++--- .../Platform/DefaultPlatformSettings.cs | 5 +- .../Platform/PlatformColorValues.cs | 64 +++++++++++++------ .../DBusPlatformSettings.cs | 9 ++- src/Avalonia.Native/NativePlatformSettings.cs | 22 ++++++- src/Avalonia.Native/avn.idl | 4 +- .../BrowserPlatformSettings.cs | 13 ++-- .../Avalonia.Browser/Interop/DomHelper.cs | 6 +- .../webapp/modules/avalonia/dom.ts | 18 ++++-- src/Windows/Avalonia.Win32/Win32Platform.cs | 11 ++++ .../Avalonia.Win32/Win32PlatformSettings.cs | 45 ++++++++++--- .../WinRT/NativeWinRTMethods.cs | 26 +++++++- src/Windows/Avalonia.Win32/WinRT/winrt.idl | 7 ++ .../Avalonia.Win32/WindowImpl.AppWndProc.cs | 8 --- src/iOS/Avalonia.iOS/PlatformSettings.cs | 11 +++- 16 files changed, 228 insertions(+), 72 deletions(-) diff --git a/native/Avalonia.Native/src/OSX/PlatformSettings.mm b/native/Avalonia.Native/src/OSX/PlatformSettings.mm index e709cd5135..fe88214c8d 100644 --- a/native/Avalonia.Native/src/OSX/PlatformSettings.mm +++ b/native/Avalonia.Native/src/OSX/PlatformSettings.mm @@ -17,15 +17,17 @@ public: if (@available(macOS 10.14, *)) { if (NSApplication.sharedApplication.effectiveAppearance.name == NSAppearanceNameAqua - || NSApplication.sharedApplication.effectiveAppearance.name == NSAppearanceNameVibrantLight - || NSApplication.sharedApplication.effectiveAppearance.name == NSAppearanceNameAccessibilityHighContrastAqua - || NSApplication.sharedApplication.effectiveAppearance.name == NSAppearanceNameAccessibilityHighContrastVibrantLight) { + || NSApplication.sharedApplication.effectiveAppearance.name == NSAppearanceNameVibrantLight) { return AvnPlatformThemeVariant::Light; } else if (NSApplication.sharedApplication.effectiveAppearance.name == NSAppearanceNameDarkAqua - || NSApplication.sharedApplication.effectiveAppearance.name == NSAppearanceNameVibrantDark - || NSApplication.sharedApplication.effectiveAppearance.name == NSAppearanceNameAccessibilityHighContrastDarkAqua - || NSApplication.sharedApplication.effectiveAppearance.name == NSAppearanceNameAccessibilityHighContrastVibrantDark) { + || 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; diff --git a/src/Android/Avalonia.Android/Platform/AndroidPlatformSettings.cs b/src/Android/Avalonia.Android/Platform/AndroidPlatformSettings.cs index f560dea3ce..0872603e7b 100644 --- a/src/Android/Avalonia.Android/Platform/AndroidPlatformSettings.cs +++ b/src/Android/Avalonia.Android/Platform/AndroidPlatformSettings.cs @@ -3,6 +3,8 @@ 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; @@ -46,11 +48,14 @@ internal class AndroidPlatformSettings : DefaultPlatformSettings 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( - systemTheme, - new Color(accent1.A, accent1.R, accent1.G, accent1.B), - new Color(accent2.A, accent2.R, accent2.G, accent2.B), - new Color(accent3.A, accent3.R, accent3.G, accent3.B)); + _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)) { @@ -58,9 +63,12 @@ internal class AndroidPlatformSettings : DefaultPlatformSettings var array = context.Theme.ObtainStyledAttributes(new[] { 16843829 }); // Resource.Attribute.ColorAccent var accent = array.GetColor(0, 0); - _latestValues = new PlatformColorValues( - systemTheme, - new Color(accent.A, accent.R, accent.G, accent.B)); + _latestValues = new PlatformColorValues + { + ThemeVariant = systemTheme, + ContrastPreference = IsHighContrast(context), + AccentColor1 = new Color(accent.A, accent.R, accent.G, accent.B) + }; array.Recycle(); } else @@ -70,4 +78,17 @@ internal class AndroidPlatformSettings : DefaultPlatformSettings 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/Avalonia.Base/Platform/DefaultPlatformSettings.cs b/src/Avalonia.Base/Platform/DefaultPlatformSettings.cs index 98f2367819..b5e7298b7e 100644 --- a/src/Avalonia.Base/Platform/DefaultPlatformSettings.cs +++ b/src/Avalonia.Base/Platform/DefaultPlatformSettings.cs @@ -31,7 +31,10 @@ namespace Avalonia.Platform public virtual PlatformColorValues GetColorValues() { - return new PlatformColorValues(PlatformThemeVariant.Light); + return new PlatformColorValues + { + ThemeVariant = PlatformThemeVariant.Light + }; } public event EventHandler? ColorValuesChanged; diff --git a/src/Avalonia.Base/Platform/PlatformColorValues.cs b/src/Avalonia.Base/Platform/PlatformColorValues.cs index 3071cc217f..d14b58e939 100644 --- a/src/Avalonia.Base/Platform/PlatformColorValues.cs +++ b/src/Avalonia.Base/Platform/PlatformColorValues.cs @@ -11,32 +11,58 @@ public enum PlatformThemeVariant Dark } +/// +/// System high contrast preference. +/// +public enum ColorContrastPreference +{ + NoPreference, + High +} + /// /// Information about current system color values, including information about dark mode and accent colors. /// -/// System theme variant or mode. -/// Primary system accent color. -/// Secondary system accent color. On some platforms can return the same value as AccentColor1. -/// Tertiary system accent color. On some platforms can return the same value as AccentColor1. -public record struct PlatformColorValues( - PlatformThemeVariant ThemeVariant, - Color AccentColor1, - Color AccentColor2, - Color AccentColor3) +public record PlatformColorValues { - public PlatformColorValues( - PlatformThemeVariant ThemeVariant, - Color AccentColor1) - : this(ThemeVariant, AccentColor1, AccentColor1, AccentColor1) + 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; } - - public PlatformColorValues(PlatformThemeVariant ThemeVariant) - : this(ThemeVariant, DefaultAccent) + + /// + /// 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; } - - private static Color DefaultAccent => new(255, 0, 120, 215); } diff --git a/src/Avalonia.FreeDesktop/DBusPlatformSettings.cs b/src/Avalonia.FreeDesktop/DBusPlatformSettings.cs index 82b4a72e74..039fc7c088 100644 --- a/src/Avalonia.FreeDesktop/DBusPlatformSettings.cs +++ b/src/Avalonia.FreeDesktop/DBusPlatformSettings.cs @@ -41,7 +41,7 @@ internal class DBusPlatformSettings : DefaultPlatformSettings { var value = await colorSchemeTask; _lastColorValues = GetColorValuesFromSetting(value); - OnColorValuesChanged(_lastColorValues.Value); + OnColorValuesChanged(_lastColorValues); } catch (Exception ex) { @@ -62,13 +62,16 @@ internal class DBusPlatformSettings : DefaultPlatformSettings 2: Prefer light appearance */ _lastColorValues = GetColorValuesFromSetting(tuple.value); - OnColorValuesChanged(_lastColorValues.Value); + OnColorValuesChanged(_lastColorValues); } } private static PlatformColorValues GetColorValuesFromSetting(object value) { var isDark = value?.ToString() == "1"; - return new PlatformColorValues(isDark ? PlatformThemeVariant.Dark : PlatformThemeVariant.Light); + return new PlatformColorValues + { + ThemeVariant = isDark ? PlatformThemeVariant.Dark : PlatformThemeVariant.Light + }; } } diff --git a/src/Avalonia.Native/NativePlatformSettings.cs b/src/Avalonia.Native/NativePlatformSettings.cs index cea748bbae..53d3da0378 100644 --- a/src/Avalonia.Native/NativePlatformSettings.cs +++ b/src/Avalonia.Native/NativePlatformSettings.cs @@ -18,16 +18,32 @@ internal class NativePlatformSettings : DefaultPlatformSettings public override PlatformColorValues GetColorValues() { - var theme = (PlatformThemeVariant)_platformSettings.PlatformTheme; + 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(theme, Color.FromUInt32(color)); + _lastColorValues = new PlatformColorValues + { + ThemeVariant = theme, + ContrastPreference = contrast, + AccentColor1 = Color.FromUInt32(color) + }; } else { - _lastColorValues = new PlatformColorValues(theme); + _lastColorValues = new PlatformColorValues + { + ThemeVariant = theme, + ContrastPreference = contrast + }; } return _lastColorValues; diff --git a/src/Avalonia.Native/avn.idl b/src/Avalonia.Native/avn.idl index 29f839c826..a062fdc61d 100644 --- a/src/Avalonia.Native/avn.idl +++ b/src/Avalonia.Native/avn.idl @@ -476,7 +476,9 @@ enum AvnWindowTransparencyMode enum AvnPlatformThemeVariant { Light, - Dark + Dark, + HighContrastLight, + HighContrastDark, } [uuid(809c652e-7396-11d2-9771-00a0c9b4d50c)] diff --git a/src/Browser/Avalonia.Browser/BrowserPlatformSettings.cs b/src/Browser/Avalonia.Browser/BrowserPlatformSettings.cs index 8a5a089752..6084c5c7de 100644 --- a/src/Browser/Avalonia.Browser/BrowserPlatformSettings.cs +++ b/src/Browser/Avalonia.Browser/BrowserPlatformSettings.cs @@ -6,21 +6,26 @@ namespace Avalonia.Browser; internal class BrowserPlatformSettings : DefaultPlatformSettings { private bool _isDarkMode; + private bool _isHighContrast; public BrowserPlatformSettings() { - _isDarkMode = DomHelper.ObserveDarkMode(m => + var obj = DomHelper.ObserveDarkMode((isDarkMode, isHighContrast) => { - _isDarkMode = m; + _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 + ThemeVariant = _isDarkMode ? PlatformThemeVariant.Dark : PlatformThemeVariant.Light, + ContrastPreference = _isHighContrast ? ColorContrastPreference.High : ColorContrastPreference.NoPreference }; } } diff --git a/src/Browser/Avalonia.Browser/Interop/DomHelper.cs b/src/Browser/Avalonia.Browser/Interop/DomHelper.cs index 090f03c4f1..9ee96768ec 100644 --- a/src/Browser/Avalonia.Browser/Interop/DomHelper.cs +++ b/src/Browser/Avalonia.Browser/Interop/DomHelper.cs @@ -27,7 +27,7 @@ internal static partial class DomHelper Action onDpiChanged); [JSImport("AvaloniaDOM.observeDarkMode", AvaloniaModule.MainModuleName)] - public static partial bool ObserveDarkMode( - [JSMarshalAs>] - Action onDpiChanged); + public static partial JSObject ObserveDarkMode( + [JSMarshalAs>] + Action onDpiChanged); } diff --git a/src/Browser/Avalonia.Browser/webapp/modules/avalonia/dom.ts b/src/Browser/Avalonia.Browser/webapp/modules/avalonia/dom.ts index 9760fccc3b..b99f8e7907 100644 --- a/src/Browser/Avalonia.Browser/webapp/modules/avalonia/dom.ts +++ b/src/Browser/Avalonia.Browser/webapp/modules/avalonia/dom.ts @@ -3,17 +3,25 @@ export class AvaloniaDOM { element.classList.add(className); } - static observeDarkMode(observer: (isDarkMode: boolean) => boolean): boolean { + static observeDarkMode(observer: (isDarkMode: boolean, isHighContrast: boolean) => boolean) { if (globalThis.matchMedia === undefined) { return false; } - const media = globalThis.matchMedia("(prefers-color-scheme: dark)"); - media.addEventListener("change", (args: MediaQueryListEvent) => { - observer(args.matches); + 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 media.matches; + return { + isDarkMode: colorShemeMedia.matches, + isHighContrast: prefersContrastMedia.matches + }; } static createAvaloniaHost(host: HTMLElement) { diff --git a/src/Windows/Avalonia.Win32/Win32Platform.cs b/src/Windows/Avalonia.Win32/Win32Platform.cs index c0a0ce4da0..26c9fc7abe 100644 --- a/src/Windows/Avalonia.Win32/Win32Platform.cs +++ b/src/Windows/Avalonia.Win32/Win32Platform.cs @@ -287,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); diff --git a/src/Windows/Avalonia.Win32/Win32PlatformSettings.cs b/src/Windows/Avalonia.Win32/Win32PlatformSettings.cs index 73ac5bf5a9..a021a3c48c 100644 --- a/src/Windows/Avalonia.Win32/Win32PlatformSettings.cs +++ b/src/Windows/Avalonia.Win32/Win32PlatformSettings.cs @@ -1,4 +1,5 @@ using System; +using System.Runtime.InteropServices; using Avalonia.Input; using Avalonia.Platform; using Avalonia.Win32.Interop; @@ -38,18 +39,44 @@ internal class Win32PlatformSettings : DefaultPlatformSettings return base.GetColorValues(); } - var settings = NativeWinRTMethods.CreateInstance("Windows.UI.ViewManagement.UISettings"); - var accent = settings.GetColorValue(UIColorType.Accent).ToAvalonia(); - var background = settings.GetColorValue(UIColorType.Background).ToAvalonia(); + var uiSettings = NativeWinRTMethods.CreateInstance("Windows.UI.ViewManagement.UISettings"); + var accent = uiSettings.GetColorValue(UIColorType.Accent).ToAvalonia(); - return _lastColorValues = new PlatformColorValues( - background.R + background.G + background.B < (255 * 3 - background.R - background.G - background.B) - ? PlatformThemeVariant.Dark - : PlatformThemeVariant.Light, - accent, accent, accent); + 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", StringComparison.OrdinalIgnoreCase) == 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(IntPtr handle) + internal void OnColorValuesChanged() { var oldColorValues = _lastColorValues; var colorValues = GetColorValues(); 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/winrt.idl b/src/Windows/Avalonia.Win32/WinRT/winrt.idl index 8dee275d50..beb88352dd 100644 --- a/src/Windows/Avalonia.Win32/WinRT/winrt.idl +++ b/src/Windows/Avalonia.Win32/WinRT/winrt.idl @@ -864,3 +864,10 @@ 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.AppWndProc.cs b/src/Windows/Avalonia.Win32/WindowImpl.AppWndProc.cs index af634fd760..8c362b0c29 100644 --- a/src/Windows/Avalonia.Win32/WindowImpl.AppWndProc.cs +++ b/src/Windows/Avalonia.Win32/WindowImpl.AppWndProc.cs @@ -732,14 +732,6 @@ namespace Avalonia.Win32 var node = AutomationNode.GetOrCreate(peer); return UiaCoreProviderApi.UiaReturnRawElementProvider(_hwnd, wParam, lParam, node); } - break; - case WindowsMessage.WM_SETTINGCHANGE: - if (Marshal.PtrToStringAuto(lParam) == "ImmersiveColorSet" - && Win32Platform.PlatformSettings is Win32PlatformSettings win32PlatformSettings) - { - win32PlatformSettings.OnColorValuesChanged(_hwnd); - } - break; } diff --git a/src/iOS/Avalonia.iOS/PlatformSettings.cs b/src/iOS/Avalonia.iOS/PlatformSettings.cs index 736e94ffc6..b8557f31bd 100644 --- a/src/iOS/Avalonia.iOS/PlatformSettings.cs +++ b/src/iOS/Avalonia.iOS/PlatformSettings.cs @@ -17,8 +17,17 @@ internal class PlatformSettings : DefaultPlatformSettings var themeVariant = UITraitCollection.CurrentTraitCollection.UserInterfaceStyle == UIUserInterfaceStyle.Dark ? PlatformThemeVariant.Dark : PlatformThemeVariant.Light; + + + var contrastPreference = UITraitCollection.CurrentTraitCollection.AccessibilityContrast == UIAccessibilityContrast.High ? + ColorContrastPreference.High : + ColorContrastPreference.NoPreference; - return _lastColorValues = new PlatformColorValues(themeVariant); + return _lastColorValues = new PlatformColorValues + { + ThemeVariant = themeVariant, + ContrastPreference = contrastPreference + }; } public void TraitCollectionDidChange() From 124c87b2cf4f15963c78511849c0c31b6398a5f0 Mon Sep 17 00:00:00 2001 From: Max Katz Date: Fri, 6 Jan 2023 23:01:13 -0500 Subject: [PATCH 26/58] Add iOS tint/accent color support --- src/iOS/Avalonia.iOS/AvaloniaView.cs | 8 ++++++ src/iOS/Avalonia.iOS/PlatformSettings.cs | 32 ++++++++++++++++++++---- 2 files changed, 35 insertions(+), 5 deletions(-) diff --git a/src/iOS/Avalonia.iOS/AvaloniaView.cs b/src/iOS/Avalonia.iOS/AvaloniaView.cs index 6d36ca7814..7442861597 100644 --- a/src/iOS/Avalonia.iOS/AvaloniaView.cs +++ b/src/iOS/Avalonia.iOS/AvaloniaView.cs @@ -60,6 +60,14 @@ namespace Avalonia.iOS settings?.TraitCollectionDidChange(); } + public override void TintColorDidChange() + { + base.TintColorDidChange(); + + var settings = AvaloniaLocator.Current.GetRequiredService() as PlatformSettings; + settings?.TraitCollectionDidChange(); + } + internal class TopLevelImpl : ITopLevelImplWithTextInputMethod, ITopLevelImplWithNativeControlHost, ITopLevelImplWithStorageProvider { diff --git a/src/iOS/Avalonia.iOS/PlatformSettings.cs b/src/iOS/Avalonia.iOS/PlatformSettings.cs index b8557f31bd..0ae290468b 100644 --- a/src/iOS/Avalonia.iOS/PlatformSettings.cs +++ b/src/iOS/Avalonia.iOS/PlatformSettings.cs @@ -22,12 +22,34 @@ internal class PlatformSettings : DefaultPlatformSettings var contrastPreference = UITraitCollection.CurrentTraitCollection.AccessibilityContrast == UIAccessibilityContrast.High ? ColorContrastPreference.High : ColorContrastPreference.NoPreference; - - return _lastColorValues = new PlatformColorValues + + UIColor? tintColor = null; + if (OperatingSystem.IsIOSVersionAtLeast(14)) + { + tintColor = UIConfigurationColorTransformer.PreferredTint(UIColor.Clear); + } + + if (tintColor is not null) { - ThemeVariant = themeVariant, - ContrastPreference = contrastPreference - }; + 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() From f7605ca22e66856a19b7c4fc465cbfe92ccc06b3 Mon Sep 17 00:00:00 2001 From: Max Katz Date: Fri, 6 Jan 2023 23:05:17 -0500 Subject: [PATCH 27/58] Fix build --- native/Avalonia.Native/src/OSX/PlatformSettings.mm | 2 +- src/Windows/Avalonia.Win32/Win32PlatformSettings.cs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/native/Avalonia.Native/src/OSX/PlatformSettings.mm b/native/Avalonia.Native/src/OSX/PlatformSettings.mm index fe88214c8d..34ceb93186 100644 --- a/native/Avalonia.Native/src/OSX/PlatformSettings.mm +++ b/native/Avalonia.Native/src/OSX/PlatformSettings.mm @@ -25,7 +25,7 @@ public: } else if (NSApplication.sharedApplication.effectiveAppearance.name == NSAppearanceNameAccessibilityHighContrastAqua || NSApplication.sharedApplication.effectiveAppearance.name == NSAppearanceNameAccessibilityHighContrastVibrantLight) { return AvnPlatformThemeVariant::HighContrastLight; - } else if (|| NSApplication.sharedApplication.effectiveAppearance.name == NSAppearanceNameAccessibilityHighContrastDarkAqua + } else if (NSApplication.sharedApplication.effectiveAppearance.name == NSAppearanceNameAccessibilityHighContrastDarkAqua || NSApplication.sharedApplication.effectiveAppearance.name == NSAppearanceNameAccessibilityHighContrastVibrantDark) { return AvnPlatformThemeVariant::HighContrastDark; } diff --git a/src/Windows/Avalonia.Win32/Win32PlatformSettings.cs b/src/Windows/Avalonia.Win32/Win32PlatformSettings.cs index a021a3c48c..0310c4bfad 100644 --- a/src/Windows/Avalonia.Win32/Win32PlatformSettings.cs +++ b/src/Windows/Avalonia.Win32/Win32PlatformSettings.cs @@ -54,7 +54,7 @@ internal class Win32PlatformSettings : DefaultPlatformSettings using var highContrastScheme = new HStringInterop(accessibilitySettings.HighContrastScheme); return _lastColorValues = new PlatformColorValues { - ThemeVariant = highContrastScheme.Value?.Contains("White", StringComparison.OrdinalIgnoreCase) == true ? + ThemeVariant = highContrastScheme.Value?.Contains("White") == true ? PlatformThemeVariant.Light : PlatformThemeVariant.Dark, ContrastPreference = ColorContrastPreference.High, From 2565538fd30a9669198b2c611b9fcdabd602bac0 Mon Sep 17 00:00:00 2001 From: Sergey Mikolaitis Date: Sun, 8 Jan 2023 03:37:17 +0300 Subject: [PATCH 28/58] [Text] improve benchmarks. add BuildManySmallTexts benchmark --- .../Text/HugeTextLayout.cs | 24 ++++++++++++++++--- 1 file changed, 21 insertions(+), 3 deletions(-) diff --git a/tests/Avalonia.Benchmarks/Text/HugeTextLayout.cs b/tests/Avalonia.Benchmarks/Text/HugeTextLayout.cs index e1b3c6a99c..e696b976ad 100644 --- a/tests/Avalonia.Benchmarks/Text/HugeTextLayout.cs +++ b/tests/Avalonia.Benchmarks/Text/HugeTextLayout.cs @@ -1,4 +1,5 @@ using System; +using System.Linq; using Avalonia.Media; using Avalonia.Media.TextFormatting; using Avalonia.UnitTests; @@ -10,9 +11,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 +39,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 +65,13 @@ 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(); + + private static TextLayout MakeLayout(string str) + => new TextLayout(str, Typeface.Default, 12d, Brushes.Black, maxWidth:120); public void Dispose() { From 289b14a48c8737d93cd383ea408f40e75f1e6a7f Mon Sep 17 00:00:00 2001 From: Max Katz Date: Sun, 8 Jan 2023 15:02:43 -0500 Subject: [PATCH 29/58] Apply suggestions from code review --- src/Browser/Avalonia.Browser/Interop/DomHelper.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Browser/Avalonia.Browser/Interop/DomHelper.cs b/src/Browser/Avalonia.Browser/Interop/DomHelper.cs index 9ee96768ec..d1133b8916 100644 --- a/src/Browser/Avalonia.Browser/Interop/DomHelper.cs +++ b/src/Browser/Avalonia.Browser/Interop/DomHelper.cs @@ -29,5 +29,5 @@ internal static partial class DomHelper [JSImport("AvaloniaDOM.observeDarkMode", AvaloniaModule.MainModuleName)] public static partial JSObject ObserveDarkMode( [JSMarshalAs>] - Action onDpiChanged); + Action observer); } From 182e469066a3da780bd8c50777127fb0fad8ebf8 Mon Sep 17 00:00:00 2001 From: rabbitism Date: Mon, 9 Jan 2023 23:51:18 +0800 Subject: [PATCH 30/58] feat: add UT for GeometryTypeConverter. --- src/Avalonia.Base/Media/Geometry.cs | 4 +- .../Converters/GeometryTypeConverterTests.cs | 54 +++++++++++++++++++ 2 files changed, 56 insertions(+), 2 deletions(-) create mode 100644 tests/Avalonia.Markup.Xaml.UnitTests/Converters/GeometryTypeConverterTests.cs diff --git a/src/Avalonia.Base/Media/Geometry.cs b/src/Avalonia.Base/Media/Geometry.cs index e2b345df1a..e62f819b74 100644 --- a/src/Avalonia.Base/Media/Geometry.cs +++ b/src/Avalonia.Base/Media/Geometry.cs @@ -8,7 +8,7 @@ namespace Avalonia.Media /// /// Defines a geometric shape. /// - [TypeConverter(typeof(GeometryConverter))] + [TypeConverter(typeof(GeometryTypeConverter))] public abstract class Geometry : AvaloniaObject { /// @@ -203,7 +203,7 @@ namespace Avalonia.Media } } - public class GeometryConverter : TypeConverter + public class GeometryTypeConverter : TypeConverter { public override bool CanConvertFrom(ITypeDescriptorContext? context, Type sourceType) { 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 }; + } + } +} From 9d075fe2a9ead45840670b2f13b322416ed626c1 Mon Sep 17 00:00:00 2001 From: Simon Cropp Date: Tue, 10 Jan 2023 07:49:29 +1100 Subject: [PATCH 31/58] Update ControlTemplateTests.cs --- .../Xaml/ControlTemplateTests.cs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/ControlTemplateTests.cs b/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/ControlTemplateTests.cs index 6c29707dcd..396a03645c 100644 --- a/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/ControlTemplateTests.cs +++ b/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/ControlTemplateTests.cs @@ -27,11 +27,11 @@ namespace Avalonia.Markup.Xaml.UnitTests.Xaml From ba90d9ab98422d40d99a4f1a6dd48c417761c150 Mon Sep 17 00:00:00 2001 From: Julien Lebosquain Date: Tue, 10 Jan 2023 01:03:51 +0100 Subject: [PATCH 32/58] Clean/Rebuild removes old resources cache from obj folder --- packages/Avalonia/AvaloniaBuildTasks.targets | 25 +++++++++++++---- .../CompileAvaloniaXamlTask.cs | 28 ++++++++++++++++--- 2 files changed, 44 insertions(+), 9 deletions(-) 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/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(); } } From b8a4de969dd6024f75d5d1a09fa51e20a02faaff Mon Sep 17 00:00:00 2001 From: Julien Lebosquain Date: Tue, 10 Jan 2023 13:55:34 +0100 Subject: [PATCH 33/58] Made resources index binary Effectively removes System.Xml.Linq dependency --- .../Platform/Internal/AssemblyDescriptor.cs | 2 +- .../Utilities/AvaloniaResourcesIndex.cs | 149 ++++++++++-------- .../Avalonia.Build.Tasks.csproj | 3 + .../GenerateAvaloniaResourcesTask.cs | 26 +-- .../XamlCompilerTaskExecutor.Helpers.cs | 21 ++- 5 files changed, 103 insertions(+), 98 deletions(-) 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/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/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); } From ae8e6cc12045aa7c4e9fe449d66eebacdd356537 Mon Sep 17 00:00:00 2001 From: Sergey Mikolaitis Date: Wed, 11 Jan 2023 02:18:20 +0300 Subject: [PATCH 34/58] [Text] [Optimization] [Alloc] Bidi Pooling --- .../Media/TextFormatting/TextFormatterImpl.cs | 4 ++-- .../TextFormatting/Unicode/BiDiAlgorithm.cs | 10 +++++++++- .../Media/TextFormatting/Unicode/BiDiData.cs | 12 +++++++++++- src/Avalonia.Base/Utilities/ArrayBuilder.cs | 18 ++++++++++++++---- 4 files changed, 36 insertions(+), 8 deletions(-) diff --git a/src/Avalonia.Base/Media/TextFormatting/TextFormatterImpl.cs b/src/Avalonia.Base/Media/TextFormatting/TextFormatterImpl.cs index ef2abdfea0..c5ded72a43 100644 --- a/src/Avalonia.Base/Media/TextFormatting/TextFormatterImpl.cs +++ b/src/Avalonia.Base/Media/TextFormatting/TextFormatterImpl.cs @@ -159,7 +159,7 @@ namespace Avalonia.Media.TextFormatting { var flowDirection = paragraphProperties.FlowDirection; var drawableTextRuns = new List(); - var biDiData = new BidiData((sbyte)flowDirection); + using var biDiData = new BidiData((sbyte)flowDirection); foreach (var textRun in textRuns) { @@ -177,7 +177,7 @@ namespace Avalonia.Media.TextFormatting } } - var biDi = new BidiAlgorithm(); + using var biDi = new BidiAlgorithm(); biDi.Process(biDiData); diff --git a/src/Avalonia.Base/Media/TextFormatting/Unicode/BiDiAlgorithm.cs b/src/Avalonia.Base/Media/TextFormatting/Unicode/BiDiAlgorithm.cs index 17ec9b1df2..3372d2be51 100644 --- a/src/Avalonia.Base/Media/TextFormatting/Unicode/BiDiAlgorithm.cs +++ b/src/Avalonia.Base/Media/TextFormatting/Unicode/BiDiAlgorithm.cs @@ -27,7 +27,7 @@ namespace Avalonia.Media.TextFormatting.Unicode /// as much as possible. /// /// - internal sealed class BidiAlgorithm + internal sealed class BidiAlgorithm : IDisposable { /// /// The original BiDiClass classes as provided by the caller @@ -1714,5 +1714,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..7e5b3a6620 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 class BidiData : IDisposable { private ArrayBuilder _classes; private ArrayBuilder _pairedBracketTypes; @@ -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/Utilities/ArrayBuilder.cs b/src/Avalonia.Base/Utilities/ArrayBuilder.cs index 60bf2c7586..d63eaddb7d 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,10 +145,11 @@ namespace Avalonia.Utilities if (newCapacity < min) { - newCapacity = (uint)min; + newCapacity = min; } - var array = new T[newCapacity]; + Dispose(); + var array = ArrayPool.Shared.Rent(newCapacity); if (_size > 0) { @@ -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); + } + } } } From 39374451ddbc733d818255c8a06b2650796b91d3 Mon Sep 17 00:00:00 2001 From: Sergey Mikolaitis Date: Wed, 11 Jan 2023 02:54:04 +0300 Subject: [PATCH 35/58] [Text] Add pooling to shapedBuffer. works only with dispose. --- src/Avalonia.Base/Media/GlyphRun.cs | 2 +- .../Media/TextFormatting/ShapedBuffer.cs | 29 ++++++++++++++++--- .../Media/TextFormatting/ShapedTextRun.cs | 8 ++++- .../Media/TextFormatting/TextLayout.cs | 10 ++++++- .../Media/TextFormatting/TextLine.cs | 4 ++- .../Media/TextFormatting/TextLineImpl.cs | 9 ++++++ src/Avalonia.Base/Utilities/ArraySlice.cs | 9 ++++++ .../Text/HugeTextLayout.cs | 8 +++-- 8 files changed, 69 insertions(+), 10 deletions(-) diff --git a/src/Avalonia.Base/Media/GlyphRun.cs b/src/Avalonia.Base/Media/GlyphRun.cs index af9e458a28..811479fde8 100644 --- a/src/Avalonia.Base/Media/GlyphRun.cs +++ b/src/Avalonia.Base/Media/GlyphRun.cs @@ -918,7 +918,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/TextFormatting/ShapedBuffer.cs b/src/Avalonia.Base/Media/TextFormatting/ShapedBuffer.cs index 902b897240..5cf153d199 100644 --- a/src/Avalonia.Base/Media/TextFormatting/ShapedBuffer.cs +++ b/src/Avalonia.Base/Media/TextFormatting/ShapedBuffer.cs @@ -1,17 +1,20 @@ 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(); + private bool _rented; public ShapedBuffer(CharacterBufferRange characterBufferRange, int bufferLength, IGlyphTypeface glyphTypeface, double fontRenderingEmSize, sbyte bidiLevel) : - this(characterBufferRange, new GlyphInfo[bufferLength], glyphTypeface, fontRenderingEmSize, bidiLevel) + this(characterBufferRange, ArrayPool.Shared.Rent(bufferLength), glyphTypeface, fontRenderingEmSize, bidiLevel) { - + _rented = true; + Length = bufferLength; } internal ShapedBuffer(CharacterBufferRange characterBufferRange, ArraySlice glyphInfos, IGlyphTypeface glyphTypeface, double fontRenderingEmSize, sbyte bidiLevel) @@ -21,11 +24,12 @@ namespace Avalonia.Media.TextFormatting 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; } @@ -260,6 +264,23 @@ namespace Avalonia.Media.TextFormatting System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator() => GetEnumerator(); } + + public void Dispose() + { + GC.SuppressFinalize(this); + if (_rented) + { + GlyphInfos.ReturnRent(); + } + } + + ~ShapedBuffer() + { + if (_rented) + { + 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..665723b284 100644 --- a/src/Avalonia.Base/Media/TextFormatting/ShapedTextRun.cs +++ b/src/Avalonia.Base/Media/TextFormatting/ShapedTextRun.cs @@ -6,7 +6,7 @@ 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; @@ -199,5 +199,11 @@ namespace Avalonia.Media.TextFormatting ShapedBuffer.GlyphClusters, BidiLevel); } + + public void Dispose() + { + _glyphRun?.Dispose(); + ShapedBuffer.Dispose(); + } } } diff --git a/src/Avalonia.Base/Media/TextFormatting/TextLayout.cs b/src/Avalonia.Base/Media/TextFormatting/TextLayout.cs index f803001481..969b960c6b 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; @@ -561,5 +561,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 5fb1171221..93ee139763 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 @@ -934,6 +935,14 @@ namespace Avalonia.Media.TextFormatting return GetTextBoundsRightToLeft(firstTextSourceIndex, textLength); } + public override void Dispose() + { + foreach (var run in _textRuns.OfType()) + { + run.Dispose(); + } + } + public TextLineImpl FinalizeLine() { _textLineMetrics = CreateLineMetrics(); 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/tests/Avalonia.Benchmarks/Text/HugeTextLayout.cs b/tests/Avalonia.Benchmarks/Text/HugeTextLayout.cs index e696b976ad..20fa268a85 100644 --- a/tests/Avalonia.Benchmarks/Text/HugeTextLayout.cs +++ b/tests/Avalonia.Benchmarks/Text/HugeTextLayout.cs @@ -70,8 +70,12 @@ In respect that the structure of the sufficient amount poses problems and challe [Benchmark] public TextLayout[] BuildManySmallTexts() => _manySmallStrings.Select(MakeLayout).ToArray(); - private static TextLayout MakeLayout(string str) - => new TextLayout(str, Typeface.Default, 12d, Brushes.Black, maxWidth:120); + 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() { From f8c2288f4a738900be1b75c6c507c403f153e056 Mon Sep 17 00:00:00 2001 From: Sergey Mikolaitis Date: Wed, 11 Jan 2023 03:04:34 +0300 Subject: [PATCH 36/58] [Text] [Bidi] fix EnsureCapacity return --- src/Avalonia.Base/Utilities/ArrayBuilder.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Avalonia.Base/Utilities/ArrayBuilder.cs b/src/Avalonia.Base/Utilities/ArrayBuilder.cs index d63eaddb7d..e6b67bd383 100644 --- a/src/Avalonia.Base/Utilities/ArrayBuilder.cs +++ b/src/Avalonia.Base/Utilities/ArrayBuilder.cs @@ -147,13 +147,13 @@ namespace Avalonia.Utilities { newCapacity = min; } - - Dispose(); + var array = ArrayPool.Shared.Rent(newCapacity); if (_size > 0) { Array.Copy(_data!, array, _size); + ArrayPool.Shared.Return(_data!); } _data = array; From dc25b802a758abd8cd6c8329224c6cb9ada45be5 Mon Sep 17 00:00:00 2001 From: Sergey Mikolaitis Date: Wed, 11 Jan 2023 03:30:06 +0300 Subject: [PATCH 37/58] [Text] add pooled list and pooled stack usage --- .../TextFormatting/Unicode/BiDiAlgorithm.cs | 22 ++++++++++++------- .../Media/TextFormatting/Unicode/BiDiData.cs | 2 +- 2 files changed, 15 insertions(+), 9 deletions(-) diff --git a/src/Avalonia.Base/Media/TextFormatting/Unicode/BiDiAlgorithm.cs b/src/Avalonia.Base/Media/TextFormatting/Unicode/BiDiAlgorithm.cs index 3372d2be51..0bea4def28 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 : IDisposable + internal struct BidiAlgorithm : IDisposable { /// /// The original BiDiClass classes as provided by the caller @@ -97,7 +98,7 @@ namespace Avalonia.Media.TextFormatting.Unicode /// The status stack used during resolution of explicit /// embedding and isolating runs /// - private readonly Stack _statusStack = new Stack(); + private readonly PooledStack _statusStack = new PooledStack(); /// /// Mapping used to virtually remove characters for rule X9 @@ -107,7 +108,7 @@ namespace Avalonia.Media.TextFormatting.Unicode /// /// Re-usable list of level runs /// - private readonly List _levelRuns = new List(); + private readonly PooledList _levelRuns = new PooledList(); /// /// Mapping for the current isolating sequence, built @@ -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 readonly PooledStack _pendingIsolateOpenings = new PooledStack(); /// /// The level of the isolating run currently being processed @@ -174,17 +175,17 @@ namespace Avalonia.Media.TextFormatting.Unicode /// Reusable list of pending opening brackets used by the /// LocatePairedBrackets method /// - private readonly List _pendingOpeningBrackets = new List(); + private readonly PooledList _pendingOpeningBrackets = new PooledList(); /// /// Resolved list of paired brackets /// - private readonly List _pairedBrackets = new List(); + private readonly PooledList _pairedBrackets = new PooledList(); /// /// Initializes a new instance of the class. /// - internal BidiAlgorithm() + public BidiAlgorithm() { } @@ -1272,7 +1273,7 @@ namespace Avalonia.Media.TextFormatting.Unicode /// Locate all pair brackets in the current isolating run /// /// A sorted list of BracketPairs - private List LocatePairedBrackets() + private PooledList LocatePairedBrackets() { // Clear work collections _pendingOpeningBrackets.Clear(); @@ -1719,8 +1720,13 @@ namespace Avalonia.Media.TextFormatting.Unicode { _workingClassesBuffer.Dispose(); _resolvedLevelsBuffer.Dispose(); + _statusStack.Dispose(); _x9Map.Dispose(); + _levelRuns.Dispose(); _isolatedRunMapping.Dispose(); + _pendingIsolateOpenings.Dispose(); + _pendingOpeningBrackets.Dispose(); + _pairedBrackets.Dispose(); } } } diff --git a/src/Avalonia.Base/Media/TextFormatting/Unicode/BiDiData.cs b/src/Avalonia.Base/Media/TextFormatting/Unicode/BiDiData.cs index 7e5b3a6620..644f7e9a8a 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 : IDisposable + internal struct BidiData : IDisposable { private ArrayBuilder _classes; private ArrayBuilder _pairedBracketTypes; From 22a41933bedfddb81c1d4db82a078263e7632a0c Mon Sep 17 00:00:00 2001 From: Sergey Mikolaitis Date: Wed, 11 Jan 2023 03:43:55 +0300 Subject: [PATCH 38/58] [Text] bidi - do not use pooled collections --- .../TextFormatting/Unicode/BiDiAlgorithm.cs | 17 ++++++----------- 1 file changed, 6 insertions(+), 11 deletions(-) diff --git a/src/Avalonia.Base/Media/TextFormatting/Unicode/BiDiAlgorithm.cs b/src/Avalonia.Base/Media/TextFormatting/Unicode/BiDiAlgorithm.cs index 0bea4def28..093c7f3f56 100644 --- a/src/Avalonia.Base/Media/TextFormatting/Unicode/BiDiAlgorithm.cs +++ b/src/Avalonia.Base/Media/TextFormatting/Unicode/BiDiAlgorithm.cs @@ -98,7 +98,7 @@ namespace Avalonia.Media.TextFormatting.Unicode /// The status stack used during resolution of explicit /// embedding and isolating runs /// - private readonly PooledStack _statusStack = new PooledStack(); + private readonly Stack _statusStack = new Stack(); /// /// Mapping used to virtually remove characters for rule X9 @@ -108,7 +108,7 @@ namespace Avalonia.Media.TextFormatting.Unicode /// /// Re-usable list of level runs /// - private readonly PooledList _levelRuns = new PooledList(); + private readonly List _levelRuns = new List(); /// /// Mapping for the current isolating sequence, built @@ -119,7 +119,7 @@ namespace Avalonia.Media.TextFormatting.Unicode /// /// A stack of pending isolate openings used by FindIsolatePairs() /// - private readonly PooledStack _pendingIsolateOpenings = new PooledStack(); + private readonly Stack _pendingIsolateOpenings = new Stack(); /// /// The level of the isolating run currently being processed @@ -175,12 +175,12 @@ namespace Avalonia.Media.TextFormatting.Unicode /// Reusable list of pending opening brackets used by the /// LocatePairedBrackets method /// - private readonly PooledList _pendingOpeningBrackets = new PooledList(); + private readonly List _pendingOpeningBrackets = new List(); /// /// Resolved list of paired brackets /// - private readonly PooledList _pairedBrackets = new PooledList(); + private readonly List _pairedBrackets = new List(); /// /// Initializes a new instance of the class. @@ -1273,7 +1273,7 @@ namespace Avalonia.Media.TextFormatting.Unicode /// Locate all pair brackets in the current isolating run /// /// A sorted list of BracketPairs - private PooledList LocatePairedBrackets() + private List LocatePairedBrackets() { // Clear work collections _pendingOpeningBrackets.Clear(); @@ -1720,13 +1720,8 @@ namespace Avalonia.Media.TextFormatting.Unicode { _workingClassesBuffer.Dispose(); _resolvedLevelsBuffer.Dispose(); - _statusStack.Dispose(); _x9Map.Dispose(); - _levelRuns.Dispose(); _isolatedRunMapping.Dispose(); - _pendingIsolateOpenings.Dispose(); - _pendingOpeningBrackets.Dispose(); - _pairedBrackets.Dispose(); } } } From 8ece18c53dcedf2475f9cbf97a25c4ea66f564e5 Mon Sep 17 00:00:00 2001 From: Sergey Mikolaitis Date: Wed, 11 Jan 2023 04:09:19 +0300 Subject: [PATCH 39/58] [Text] optimize CoalesceLevels alloc --- .../Media/TextFormatting/TextFormatterImpl.cs | 21 +++++++++---------- 1 file changed, 10 insertions(+), 11 deletions(-) diff --git a/src/Avalonia.Base/Media/TextFormatting/TextFormatterImpl.cs b/src/Avalonia.Base/Media/TextFormatting/TextFormatterImpl.cs index a69c56eee1..517372648f 100644 --- a/src/Avalonia.Base/Media/TextFormatting/TextFormatterImpl.cs +++ b/src/Avalonia.Base/Media/TextFormatting/TextFormatterImpl.cs @@ -187,10 +187,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++) { @@ -282,12 +279,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; @@ -306,7 +305,7 @@ namespace Avalonia.Media.TextFormatting { var drawableRun = textCharacters[i]; - yield return new[] { drawableRun }; + processedRuns.Add(drawableRun); levelIndex += drawableRun.Length; @@ -329,7 +328,7 @@ namespace Avalonia.Media.TextFormatting if (j == runText.Length) { - yield return currentRun.GetShapeableCharacters(runText.Take(j), runLevel, ref previousProperties); + processedRuns.AddRange(currentRun.GetShapeableCharacters(runText.Take(j), runLevel, ref previousProperties)); runLevel = levels[levelIndex]; @@ -342,7 +341,7 @@ namespace Avalonia.Media.TextFormatting } // End of this run - yield return currentRun.GetShapeableCharacters(runText.Take(j), runLevel, ref previousProperties); + processedRuns.AddRange(currentRun.GetShapeableCharacters(runText.Take(j), runLevel, ref previousProperties)); runText = runText.Skip(j); @@ -355,10 +354,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)); } /// From 31beefe487a3f3d09027d835c7897d0d082947f6 Mon Sep 17 00:00:00 2001 From: Sergey Mikolaitis Date: Wed, 11 Jan 2023 04:12:02 +0300 Subject: [PATCH 40/58] [Bidi] Optimize alloc of double dictionary --- .../Media/TextFormatting/Unicode/BiDiAlgorithm.cs | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/src/Avalonia.Base/Media/TextFormatting/Unicode/BiDiAlgorithm.cs b/src/Avalonia.Base/Media/TextFormatting/Unicode/BiDiAlgorithm.cs index 093c7f3f56..8014762ce8 100644 --- a/src/Avalonia.Base/Media/TextFormatting/Unicode/BiDiAlgorithm.cs +++ b/src/Avalonia.Base/Media/TextFormatting/Unicode/BiDiAlgorithm.cs @@ -67,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 @@ -228,7 +228,7 @@ namespace Avalonia.Media.TextFormatting.Unicode ArraySlice? outLevels) { // Reset state - _isolatePairs.Clear(); + _isolatePairs?.Clear(); _workingClassesBuffer.Clear(); _levelRuns.Clear(); _resolvedLevelsBuffer.Clear(); @@ -324,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; } @@ -379,6 +379,7 @@ namespace Avalonia.Media.TextFormatting.Unicode { if (_pendingIsolateOpenings.Count > 0) { + _isolatePairs ??= new BidiDictionary(); _isolatePairs.Add(_pendingIsolateOpenings.Pop(), i); } @@ -499,7 +500,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; } @@ -830,7 +831,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); From 97f2a26941c492cd7bcc5447dd78aac60ce12b80 Mon Sep 17 00:00:00 2001 From: Sergey Mikolaitis Date: Wed, 11 Jan 2023 04:18:01 +0300 Subject: [PATCH 41/58] [Text] [Bidi] do not create collections if not need them --- .../Media/TextFormatting/Unicode/BiDiAlgorithm.cs | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/Avalonia.Base/Media/TextFormatting/Unicode/BiDiAlgorithm.cs b/src/Avalonia.Base/Media/TextFormatting/Unicode/BiDiAlgorithm.cs index 8014762ce8..100d381afe 100644 --- a/src/Avalonia.Base/Media/TextFormatting/Unicode/BiDiAlgorithm.cs +++ b/src/Avalonia.Base/Media/TextFormatting/Unicode/BiDiAlgorithm.cs @@ -119,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 @@ -359,7 +359,7 @@ namespace Avalonia.Media.TextFormatting.Unicode _hasIsolates = false; // BD9... - _pendingIsolateOpenings.Clear(); + _pendingIsolateOpenings?.Clear(); for (var i = 0; i < _originalClasses.Length; i++) { @@ -371,13 +371,14 @@ 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); From fc4e549c2eb480aa1339befc88a6473bd71a320c Mon Sep 17 00:00:00 2001 From: Benedikt Stebner Date: Wed, 11 Jan 2023 13:31:01 +0100 Subject: [PATCH 42/58] Cleanup --- src/Avalonia.Base/Media/TextDecoration.cs | 57 ++++++++++++++++++- src/Avalonia.Base/Platform/IGlyphRunImpl.cs | 6 +- .../HeadlessPlatformRenderInterface.cs | 5 ++ src/Skia/Avalonia.Skia/GlyphRunImpl.cs | 4 ++ .../Avalonia.Direct2D1/Media/GlyphRunImpl.cs | 8 ++- tests/Avalonia.UnitTests/MockGlyphRun.cs | 8 ++- 6 files changed, 82 insertions(+), 6 deletions(-) diff --git a/src/Avalonia.Base/Media/TextDecoration.cs b/src/Avalonia.Base/Media/TextDecoration.cs index 0a7328125a..0ed73e7786 100644 --- a/src/Avalonia.Base/Media/TextDecoration.cs +++ b/src/Avalonia.Base/Media/TextDecoration.cs @@ -1,5 +1,9 @@ -using Avalonia.Collections; +using System.Collections.Generic; +using Avalonia.Collections; +using Avalonia.Collections.Pooled; using Avalonia.Media.TextFormatting; +using Avalonia.Platform; +using Avalonia.Utilities; namespace Avalonia.Media { @@ -196,19 +200,66 @@ namespace Avalonia.Media break; } + var strokeOffset = 0.0; + switch (StrokeOffsetUnit) { case TextDecorationUnit.FontRenderingEmSize: - origin += new Point(0, StrokeOffset * textMetrics.FontRenderingEmSize); + strokeOffset = StrokeOffset * textMetrics.FontRenderingEmSize; break; case TextDecorationUnit.Pixel: - origin += new Point(0, StrokeOffset); + strokeOffset = StrokeOffset; break; } + origin += new Point(0, strokeOffset); + var pen = new Pen(Stroke ?? defaultBrush, thickness, new DashStyle(StrokeDashArray, StrokeDashOffset), StrokeLineCap); + if (Location == TextDecorationLocation.Underline && MathUtilities.IsZero(strokeOffset)) + { + var intersections = glyphRun.GlyphRunImpl.GetIntersections((float)(thickness * 0.5d + strokeOffset), (float)(thickness * 1.5d + strokeOffset)); + + 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/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.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/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/Windows/Avalonia.Direct2D1/Media/GlyphRunImpl.cs b/src/Windows/Avalonia.Direct2D1/Media/GlyphRunImpl.cs index 0b06d5ef3e..638aaf08d6 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 { @@ -15,5 +16,10 @@ namespace Avalonia.Direct2D1.Media { GlyphRun?.Dispose(); } + + public IReadOnlyList GetIntersections(float lowerBound, float upperBound) + { + return null; + } } } 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; + } } } From f6535c6b26845f7e327ab494e5ff50b53fb108d8 Mon Sep 17 00:00:00 2001 From: Sergey Mikolaitis Date: Wed, 11 Jan 2023 16:01:37 +0300 Subject: [PATCH 43/58] [Text] Dispose Text Layout in TextBlock and TextPresenter --- src/Avalonia.Controls/Presenters/TextPresenter.cs | 3 +++ src/Avalonia.Controls/TextBlock.cs | 11 ++++------- 2 files changed, 7 insertions(+), 7 deletions(-) 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/TextBlock.cs b/src/Avalonia.Controls/TextBlock.cs index 7a0da1ecc5..31ddf1f985 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) From 56e94e424c9b120f8fac3aa450bbe43ceae22d81 Mon Sep 17 00:00:00 2001 From: Sergey Mikolaitis Date: Wed, 11 Jan 2023 16:35:23 +0300 Subject: [PATCH 44/58] [Text] Add textblocks virtualization benchmark, rename field --- .../Media/TextFormatting/ShapedBuffer.cs | 8 +++---- .../Text/HugeTextLayout.cs | 21 +++++++++++++++++++ 2 files changed, 25 insertions(+), 4 deletions(-) diff --git a/src/Avalonia.Base/Media/TextFormatting/ShapedBuffer.cs b/src/Avalonia.Base/Media/TextFormatting/ShapedBuffer.cs index 5cf153d199..40500670ad 100644 --- a/src/Avalonia.Base/Media/TextFormatting/ShapedBuffer.cs +++ b/src/Avalonia.Base/Media/TextFormatting/ShapedBuffer.cs @@ -8,12 +8,12 @@ namespace Avalonia.Media.TextFormatting public sealed class ShapedBuffer : IList, IDisposable { private static readonly IComparer s_clusterComparer = new CompareClusters(); - private bool _rented; + private bool _bufferRented; public ShapedBuffer(CharacterBufferRange characterBufferRange, int bufferLength, IGlyphTypeface glyphTypeface, double fontRenderingEmSize, sbyte bidiLevel) : this(characterBufferRange, ArrayPool.Shared.Rent(bufferLength), glyphTypeface, fontRenderingEmSize, bidiLevel) { - _rented = true; + _bufferRented = true; Length = bufferLength; } @@ -268,7 +268,7 @@ namespace Avalonia.Media.TextFormatting public void Dispose() { GC.SuppressFinalize(this); - if (_rented) + if (_bufferRented) { GlyphInfos.ReturnRent(); } @@ -276,7 +276,7 @@ namespace Avalonia.Media.TextFormatting ~ShapedBuffer() { - if (_rented) + if (_bufferRented) { GlyphInfos.ReturnRent(); } diff --git a/tests/Avalonia.Benchmarks/Text/HugeTextLayout.cs b/tests/Avalonia.Benchmarks/Text/HugeTextLayout.cs index 20fa268a85..bf0253e9ab 100644 --- a/tests/Avalonia.Benchmarks/Text/HugeTextLayout.cs +++ b/tests/Avalonia.Benchmarks/Text/HugeTextLayout.cs @@ -1,5 +1,6 @@ using System; using System.Linq; +using Avalonia.Controls; using Avalonia.Media; using Avalonia.Media.TextFormatting; using Avalonia.UnitTests; @@ -70,6 +71,26 @@ In respect that the structure of the sufficient amount poses problems and challe [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); From 91fd89017e3f0b43886422715b091c42cacf034b Mon Sep 17 00:00:00 2001 From: Sergey Mikolaitis Date: Wed, 11 Jan 2023 17:34:48 +0300 Subject: [PATCH 45/58] [Text] do not use linq on dispose --- src/Avalonia.Base/Media/TextFormatting/TextLineImpl.cs | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/Avalonia.Base/Media/TextFormatting/TextLineImpl.cs b/src/Avalonia.Base/Media/TextFormatting/TextLineImpl.cs index af954eb5e9..6034e9f679 100644 --- a/src/Avalonia.Base/Media/TextFormatting/TextLineImpl.cs +++ b/src/Avalonia.Base/Media/TextFormatting/TextLineImpl.cs @@ -981,9 +981,12 @@ namespace Avalonia.Media.TextFormatting public override void Dispose() { - foreach (var run in _textRuns.OfType()) + for (int i = 0; i < _textRuns.Count; i++) { - run.Dispose(); + if (_textRuns[i] is ShapedTextRun shapedTextRun) + { + shapedTextRun.Dispose(); + } } } From 74bbfd8509c34c82d7f73f9bd1e85f9e1801414a Mon Sep 17 00:00:00 2001 From: Benedikt Stebner Date: Wed, 11 Jan 2023 15:35:07 +0100 Subject: [PATCH 46/58] Support intercepts for all location but TextDecorationLocation.Strikethrough --- src/Avalonia.Base/Media/TextDecoration.cs | 19 +++++++++---------- tests/Avalonia.Benchmarks/NullGlyphRun.cs | 8 +++++++- 2 files changed, 16 insertions(+), 11 deletions(-) diff --git a/src/Avalonia.Base/Media/TextDecoration.cs b/src/Avalonia.Base/Media/TextDecoration.cs index 0ed73e7786..d6b2841214 100644 --- a/src/Avalonia.Base/Media/TextDecoration.cs +++ b/src/Avalonia.Base/Media/TextDecoration.cs @@ -1,4 +1,5 @@ -using System.Collections.Generic; +using System; +using System.Collections.Generic; using Avalonia.Collections; using Avalonia.Collections.Pooled; using Avalonia.Media.TextFormatting; @@ -200,26 +201,24 @@ namespace Avalonia.Media break; } - var strokeOffset = 0.0; - switch (StrokeOffsetUnit) { case TextDecorationUnit.FontRenderingEmSize: - strokeOffset = StrokeOffset * textMetrics.FontRenderingEmSize; + origin += new Point(0, StrokeOffset * textMetrics.FontRenderingEmSize); break; case TextDecorationUnit.Pixel: - strokeOffset = StrokeOffset; + origin += new Point(0, StrokeOffset); break; } - origin += new Point(0, strokeOffset); - var pen = new Pen(Stroke ?? defaultBrush, thickness, new DashStyle(StrokeDashArray, StrokeDashOffset), StrokeLineCap); - if (Location == TextDecorationLocation.Underline && MathUtilities.IsZero(strokeOffset)) + if (Location != TextDecorationLocation.Strikethrough) { - var intersections = glyphRun.GlyphRunImpl.GetIntersections((float)(thickness * 0.5d + strokeOffset), (float)(thickness * 1.5d + strokeOffset)); + 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) { @@ -257,7 +256,7 @@ namespace Avalonia.Media } return; - } + } } drawingContext.DrawLine(pen, origin, origin + new Point(glyphRun.Metrics.Width, 0)); 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; + } } } From 13280eecf51d2055936d48684643b232d5f8277c Mon Sep 17 00:00:00 2001 From: Benedikt Stebner Date: Wed, 11 Jan 2023 16:29:01 +0100 Subject: [PATCH 47/58] Fix TextLineMetrics calculation for TextLines that contain a TextEndOfLine run --- .../Media/TextFormatting/TextLineImpl.cs | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/src/Avalonia.Base/Media/TextFormatting/TextLineImpl.cs b/src/Avalonia.Base/Media/TextFormatting/TextLineImpl.cs index a1f93bcd07..e7efa33435 100644 --- a/src/Avalonia.Base/Media/TextFormatting/TextLineImpl.cs +++ b/src/Avalonia.Base/Media/TextFormatting/TextLineImpl.cs @@ -1436,6 +1436,13 @@ namespace Avalonia.Media.TextFormatting var lineHeight = _paragraphProperties.LineHeight; + var lastRunIndex = _textRuns.Count - 1; + + if (_textRuns[lastRunIndex] is TextEndOfLine && lastRunIndex > 0) + { + lastRunIndex--; + } + for (var index = 0; index < _textRuns.Count; index++) { switch (_textRuns[index]) @@ -1470,7 +1477,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 +1497,7 @@ namespace Avalonia.Media.TextFormatting { case FlowDirection.LeftToRight: { - if (index == _textRuns.Count - 1) + if (index == lastRunIndex) { width = widthIncludingWhitespace; trailingWhitespaceLength = 0; @@ -1502,7 +1509,7 @@ namespace Avalonia.Media.TextFormatting case FlowDirection.RightToLeft: { - if (index == _textRuns.Count - 1) + if (index == lastRunIndex) { width = widthIncludingWhitespace; trailingWhitespaceLength = 0; From 55f33e5c444caef23e9805f4af5b51d37f099883 Mon Sep 17 00:00:00 2001 From: Sergey Mikolaitis Date: Wed, 11 Jan 2023 20:21:00 +0300 Subject: [PATCH 48/58] [Text] fix tests for ShapedBuffer pooling --- src/Avalonia.Base/Media/TextFormatting/ShapedBuffer.cs | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/Avalonia.Base/Media/TextFormatting/ShapedBuffer.cs b/src/Avalonia.Base/Media/TextFormatting/ShapedBuffer.cs index 5cf153d199..0e3267ee38 100644 --- a/src/Avalonia.Base/Media/TextFormatting/ShapedBuffer.cs +++ b/src/Avalonia.Base/Media/TextFormatting/ShapedBuffer.cs @@ -11,7 +11,11 @@ namespace Avalonia.Media.TextFormatting private bool _rented; public ShapedBuffer(CharacterBufferRange characterBufferRange, int bufferLength, IGlyphTypeface glyphTypeface, double fontRenderingEmSize, sbyte bidiLevel) : - this(characterBufferRange, ArrayPool.Shared.Rent(bufferLength), glyphTypeface, fontRenderingEmSize, bidiLevel) + this(characterBufferRange, + new ArraySlice(ArrayPool.Shared.Rent(bufferLength), 0, bufferLength), + glyphTypeface, + fontRenderingEmSize, + bidiLevel) { _rented = true; Length = bufferLength; From 1e646fd9a0431fa1c5b72f384232cff118f6c2e4 Mon Sep 17 00:00:00 2001 From: Benedikt Stebner Date: Wed, 11 Jan 2023 18:47:30 +0100 Subject: [PATCH 49/58] Prevent a crash for empty lines --- src/Avalonia.Base/Media/TextFormatting/TextLineImpl.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Avalonia.Base/Media/TextFormatting/TextLineImpl.cs b/src/Avalonia.Base/Media/TextFormatting/TextLineImpl.cs index e7efa33435..72a8ec8941 100644 --- a/src/Avalonia.Base/Media/TextFormatting/TextLineImpl.cs +++ b/src/Avalonia.Base/Media/TextFormatting/TextLineImpl.cs @@ -1438,7 +1438,7 @@ namespace Avalonia.Media.TextFormatting var lastRunIndex = _textRuns.Count - 1; - if (_textRuns[lastRunIndex] is TextEndOfLine && lastRunIndex > 0) + if (lastRunIndex > 0 && _textRuns[lastRunIndex] is TextEndOfLine) { lastRunIndex--; } From 95ccb0c9c69dda3873a2a1ec1e0ce8f922f1d8c2 Mon Sep 17 00:00:00 2001 From: Martijn Sneijders Date: Thu, 12 Jan 2023 20:24:28 +0100 Subject: [PATCH 50/58] ScrollGestureRecognizer ends its current fling gesture (touch or pen) if ScrollGestureEventArgs is no longer handled by the Presenter. --- .../Input/GestureRecognizers/ScrollGestureRecognizer.cs | 9 ++++++++- src/Avalonia.Base/Input/ScrollGestureEventArgs.cs | 4 ++++ .../Presenters/ScrollContentPresenter.cs | 2 ++ 3 files changed, 14 insertions(+), 1 deletion(-) 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/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.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; } } From 79b30658f15b11820a5a086923d3fed2bbdf2a07 Mon Sep 17 00:00:00 2001 From: Emmanuel Hansen Date: Fri, 13 Jan 2023 10:19:45 +0000 Subject: [PATCH 51/58] add TopLevel.GetTopLevel api --- samples/ControlCatalog/Pages/DialogsPage.xaml.cs | 2 +- samples/ControlCatalog/Pages/NotificationsPage.xaml.cs | 2 +- src/Avalonia.Controls/ComboBox.cs | 2 +- src/Avalonia.Controls/Primitives/OverlayPopupHost.cs | 2 +- src/Avalonia.Controls/Primitives/Popup.cs | 2 +- src/Avalonia.Controls/TopLevel.cs | 10 ++++++++++ src/Avalonia.Native/AvaloniaNativeDragSource.cs | 3 +-- src/Windows/Avalonia.Win32/Input/WindowsMouseDevice.cs | 3 +-- 8 files changed, 17 insertions(+), 9 deletions(-) diff --git a/samples/ControlCatalog/Pages/DialogsPage.xaml.cs b/samples/ControlCatalog/Pages/DialogsPage.xaml.cs index 67e9ef4e40..46f25f4d8d 100644 --- a/samples/ControlCatalog/Pages/DialogsPage.xaml.cs +++ b/samples/ControlCatalog/Pages/DialogsPage.xaml.cs @@ -397,7 +397,7 @@ CanPickFolder: {storageProvider.CanPickFolder}"; } Window GetWindow() => this.VisualRoot as Window ?? throw new NullReferenceException("Invalid Owner"); - TopLevel GetTopLevel() => this.VisualRoot as TopLevel ?? 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/Avalonia.Controls/ComboBox.cs b/src/Avalonia.Controls/ComboBox.cs index 8da99b5f25..6018779e34 100644 --- a/src/Avalonia.Controls/ComboBox.cs +++ b/src/Avalonia.Controls/ComboBox.cs @@ -383,7 +383,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/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/TopLevel.cs b/src/Avalonia.Controls/TopLevel.cs index 1f5b8f80d2..9970a0f1c7 100644 --- a/src/Avalonia.Controls/TopLevel.cs +++ b/src/Avalonia.Controls/TopLevel.cs @@ -341,6 +341,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 . 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/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) From 7cd51c91615e3b02e8aa0827d179e77c315db89c Mon Sep 17 00:00:00 2001 From: Max Katz Date: Fri, 13 Jan 2023 14:34:22 -0500 Subject: [PATCH 52/58] Update samples/ControlCatalog/Pages/DialogsPage.xaml.cs --- samples/ControlCatalog/Pages/DialogsPage.xaml.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/samples/ControlCatalog/Pages/DialogsPage.xaml.cs b/samples/ControlCatalog/Pages/DialogsPage.xaml.cs index 46f25f4d8d..8db6e76dca 100644 --- a/samples/ControlCatalog/Pages/DialogsPage.xaml.cs +++ b/samples/ControlCatalog/Pages/DialogsPage.xaml.cs @@ -396,7 +396,7 @@ CanPickFolder: {storageProvider.CanPickFolder}"; return item.TryGetUri(out var uri) ? uri.ToString() : item.Name; } - Window GetWindow() => this.VisualRoot as Window ?? 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() From 473cda2d3e92e511a4f1917d18c5cb641394b426 Mon Sep 17 00:00:00 2001 From: Benedikt Stebner Date: Fri, 13 Jan 2023 21:56:10 +0100 Subject: [PATCH 53/58] [Direct2D1] Don't dispose DirectWrite GlyphRun explicitly --- src/Windows/Avalonia.Direct2D1/Media/GlyphRunImpl.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/Windows/Avalonia.Direct2D1/Media/GlyphRunImpl.cs b/src/Windows/Avalonia.Direct2D1/Media/GlyphRunImpl.cs index 638aaf08d6..67418613a4 100644 --- a/src/Windows/Avalonia.Direct2D1/Media/GlyphRunImpl.cs +++ b/src/Windows/Avalonia.Direct2D1/Media/GlyphRunImpl.cs @@ -14,7 +14,8 @@ namespace Avalonia.Direct2D1.Media public void Dispose() { - GlyphRun?.Dispose(); + //SharpDX already handles this. + //GlyphRun?.Dispose(); } public IReadOnlyList GetIntersections(float lowerBound, float upperBound) From dec8c1c586be564857dea712c36851918267fe80 Mon Sep 17 00:00:00 2001 From: Julien Lebosquain Date: Fri, 13 Jan 2023 16:33:28 +0100 Subject: [PATCH 54/58] Used Memory/Span instead of CharacterBufferReference/Range + GlyphRun uses ReadOnlyMemory for characters --- src/Avalonia.Base/Media/FormattedText.cs | 7 +- src/Avalonia.Base/Media/GlyphRun.cs | 93 +++--- .../TextFormatting/CharacterBufferRange.cs | 275 ------------------ .../CharacterBufferReference.cs | 115 -------- .../TextFormatting/FormattedTextSource.cs | 14 +- .../TextFormatting/InterWordJustification.cs | 6 +- .../Media/TextFormatting/ShapedBuffer.cs | 24 +- .../Media/TextFormatting/ShapedTextRun.cs | 16 +- .../Media/TextFormatting/TextCharacters.cs | 123 +++----- .../TextFormatting/TextEllipsisHelper.cs | 4 +- .../Media/TextFormatting/TextFormatterImpl.cs | 153 ++++++---- .../Media/TextFormatting/TextRun.cs | 21 +- .../Media/TextFormatting/TextShaper.cs | 6 +- .../Media/TextFormatting/Unicode/BiDiData.cs | 2 +- .../Media/TextFormatting/Unicode/Codepoint.cs | 67 +---- .../Unicode/CodepointEnumerator.cs | 6 +- .../Media/TextFormatting/Unicode/Grapheme.cs | 14 +- .../Unicode/GraphemeEnumerator.cs | 14 +- .../Unicode/LineBreakEnumerator.cs | 13 +- .../Media/TextFormatting/UnshapedTextRun.cs | 44 +-- src/Avalonia.Base/Platform/ITextShaperImpl.cs | 6 +- src/Avalonia.Controls/TextBlock.cs | 15 +- src/Avalonia.Controls/TextBox.cs | 6 +- .../TextBoxTextInputMethodClient.cs | 6 +- .../HeadlessPlatformStubs.cs | 6 +- src/Skia/Avalonia.Skia/TextShaperImpl.cs | 33 ++- .../Media/TextShaperImpl.cs | 40 ++- .../Media/TextFormatting/BiDiClassTests.cs | 2 +- .../GraphemeBreakClassTrieGeneratorTests.cs | 15 +- .../LineBreakEnumuratorTests.cs | 8 +- .../Presenters/TextPresenter_Tests.cs | 2 +- .../Media/GlyphRunTests.cs | 12 +- .../TextFormatting/SingleBufferTextSource.cs | 11 +- .../TextFormatting/TextFormatterTests.cs | 4 +- .../Media/TextFormatting/TextLayoutTests.cs | 38 +-- .../Media/TextFormatting/TextLineTests.cs | 9 +- .../Media/TextFormatting/TextShaperTests.cs | 2 +- .../HarfBuzzTextShaperImpl.cs | 69 +++-- .../Avalonia.UnitTests/MockTextShaperImpl.cs | 14 +- tests/Avalonia.UnitTests/TextTestHelper.cs | 15 + 40 files changed, 457 insertions(+), 873 deletions(-) delete mode 100644 src/Avalonia.Base/Media/TextFormatting/CharacterBufferRange.cs delete mode 100644 src/Avalonia.Base/Media/TextFormatting/CharacterBufferReference.cs create mode 100644 tests/Avalonia.UnitTests/TextTestHelper.cs 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/GlyphRun.cs b/src/Avalonia.Base/Media/GlyphRun.cs index 811479fde8..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; 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 af896b426d..b05fab08fa 100644 --- a/src/Avalonia.Base/Media/TextFormatting/ShapedBuffer.cs +++ b/src/Avalonia.Base/Media/TextFormatting/ShapedBuffer.cs @@ -9,21 +9,21 @@ namespace Avalonia.Media.TextFormatting { private static readonly IComparer s_clusterComparer = new CompareClusters(); private bool _bufferRented; - - public ShapedBuffer(CharacterBufferRange characterBufferRange, int bufferLength, IGlyphTypeface glyphTypeface, double fontRenderingEmSize, sbyte bidiLevel) : - this(characterBufferRange, - new ArraySlice(ArrayPool.Shared.Rent(bufferLength), 0, bufferLength), - glyphTypeface, - fontRenderingEmSize, + + 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; @@ -51,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. @@ -113,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); } @@ -125,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); diff --git a/src/Avalonia.Base/Media/TextFormatting/ShapedTextRun.cs b/src/Avalonia.Base/Media/TextFormatting/ShapedTextRun.cs index 665723b284..583f2e49f1 100644 --- a/src/Avalonia.Base/Media/TextFormatting/ShapedTextRun.cs +++ b/src/Avalonia.Base/Media/TextFormatting/ShapedTextRun.cs @@ -13,8 +13,6 @@ namespace Avalonia.Media.TextFormatting 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,7 +194,7 @@ namespace Avalonia.Media.TextFormatting return new GlyphRun( ShapedBuffer.GlyphTypeface, ShapedBuffer.FontRenderingEmSize, - new CharacterBufferRange(CharacterBufferReference, Length), + Text, ShapedBuffer.GlyphIndices, ShapedBuffer.GlyphAdvances, ShapedBuffer.GlyphOffsets, 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 517372648f..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; @@ -162,17 +164,13 @@ namespace Avalonia.Media.TextFormatting 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); } } @@ -198,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) { @@ -209,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; } @@ -237,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; } @@ -253,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++) { @@ -294,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++) { @@ -312,11 +368,12 @@ namespace Avalonia.Media.TextFormatting 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) { @@ -326,9 +383,9 @@ namespace Avalonia.Media.TextFormatting levelIndex++; j += count; - if (j == runText.Length) + if (j == runTextSpan.Length) { - processedRuns.AddRange(currentRun.GetShapeableCharacters(runText.Take(j), runLevel, ref previousProperties)); + processedRuns.AddRange(currentRun.GetShapeableCharacters(runText.Slice(0, j), runLevel, ref previousProperties)); runLevel = levels[levelIndex]; @@ -341,9 +398,10 @@ namespace Avalonia.Media.TextFormatting } // End of this run - processedRuns.AddRange(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; @@ -411,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); @@ -442,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()) { @@ -541,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) }; @@ -589,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()) { @@ -651,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 @@ -771,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/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/BiDiData.cs b/src/Avalonia.Base/Media/TextFormatting/Unicode/BiDiData.cs index 644f7e9a8a..0f0b3235e1 100644 --- a/src/Avalonia.Base/Media/TextFormatting/Unicode/BiDiData.cs +++ b/src/Avalonia.Base/Media/TextFormatting/Unicode/BiDiData.cs @@ -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); 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/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.Controls/TextBlock.cs b/src/Avalonia.Controls/TextBlock.cs index 31ddf1f985..9bd1dc95f9 100644 --- a/src/Avalonia.Controls/TextBlock.cs +++ b/src/Avalonia.Controls/TextBlock.cs @@ -826,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; } @@ -842,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); } } @@ -884,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.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/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/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/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.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.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/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; + } + } +} From 5936f52b2c0cba31d11bf366e74dfedfda299a12 Mon Sep 17 00:00:00 2001 From: Emmanuel Hansen Date: Tue, 3 Jan 2023 13:48:36 +0000 Subject: [PATCH 55/58] Add back requested event for TopLevel --- .../Avalonia.Android/AvaloniaMainActivity.cs | 19 ++++--- .../IAndroidNavigationService.cs | 14 +++++ .../AndroidSystemNavigationManager.cs | 28 ++++++++++ .../Platform/SkiaPlatform/TopLevelImpl.cs | 7 ++- .../Platform/SystemNavigationManager.cs | 18 ++++++ src/Avalonia.Controls/TopLevel.cs | 56 +++++++++++++++++++ 6 files changed, 134 insertions(+), 8 deletions(-) create mode 100644 src/Android/Avalonia.Android/IAndroidNavigationService.cs create mode 100644 src/Android/Avalonia.Android/Platform/AndroidSystemNavigationManager.cs create mode 100644 src/Avalonia.Base/Platform/SystemNavigationManager.cs 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/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/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..c504a951e5 100644 --- a/src/Android/Avalonia.Android/Platform/SkiaPlatform/TopLevelImpl.cs +++ b/src/Android/Avalonia.Android/Platform/SkiaPlatform/TopLevelImpl.cs @@ -31,7 +31,8 @@ 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 +58,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) => @@ -300,6 +303,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/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.Controls/TopLevel.cs b/src/Avalonia.Controls/TopLevel.cs index 9970a0f1c7..66c108b436 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; @@ -76,6 +77,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 +96,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 +213,44 @@ 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); + }; + } + + var backKeyGesture = new KeyGesture(Key.Left, KeyModifiers.Alt); + _backGestureSubscription = _inputManager?.PreProcess.OfType().Subscribe(e => + { + bool backRequested = false; + + if (e is RawKeyEventArgs rawKeyEventArgs && rawKeyEventArgs.Type == RawKeyEventType.KeyDown) + { + var keyEvent = new KeyEventArgs() + { + KeyModifiers = (KeyModifiers)rawKeyEventArgs.Modifiers, + Key = rawKeyEventArgs.Key + }; + + backRequested = backKeyGesture.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 +309,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 @@ -382,6 +437,7 @@ namespace Avalonia.Controls _pointerOverPreProcessor?.OnCompleted(); _pointerOverPreProcessorSubscription?.Dispose(); + _backGestureSubscription?.Dispose(); PlatformImpl = null; From a75753c9426c5c47b490c24511d082704e244a10 Mon Sep 17 00:00:00 2001 From: Dmitry Zhelnin Date: Fri, 6 Jan 2023 16:28:34 +0300 Subject: [PATCH 56/58] EventRoute: remove additional check if event was handled --- src/Avalonia.Base/Interactivity/EventRoute.cs | 5 +--- .../Interactivity/InteractiveTests.cs | 26 +++++++++++++++++++ 2 files changed, 27 insertions(+), 4 deletions(-) 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/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; + } } } } From 1a0492dcb0a239432a851c829179b94120391612 Mon Sep 17 00:00:00 2001 From: Giuseppe Lippolis Date: Mon, 16 Jan 2023 12:24:51 +0100 Subject: [PATCH 57/58] fix: Warning CS0105: The using directive for 'Avalonia.Reactive' appeared previously in this namespace --- src/Avalonia.Controls.DataGrid/Utils/CellEditBinding.cs | 1 - src/Avalonia.FreeDesktop/LinuxMountedVolumeInfoListener.cs | 4 ---- src/Avalonia.Native/MacOSMountedVolumeInfoProvider.cs | 1 - .../Avalonia.Win32/WindowsMountedVolumeInfoListener.cs | 1 - 4 files changed, 7 deletions(-) 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.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.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/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; From ac206db0980fd3483b3dc3c5db571ec512f965ca Mon Sep 17 00:00:00 2001 From: Emmanuel Hansen Date: Mon, 16 Jan 2023 14:14:53 +0000 Subject: [PATCH 58/58] add browser support --- .../Platform/PlatformHotkeyConfiguration.cs | 5 ++++ src/Avalonia.Controls/TopLevel.cs | 20 ++++++++++------ .../BrowserSystemNavigationManager.cs | 24 +++++++++++++++++++ .../Avalonia.Browser/BrowserTopLevelImpl.cs | 5 +++- .../Interop/NavigationHelper.cs | 10 ++++++++ .../webapp/modules/avalonia.ts | 5 +++- .../modules/avalonia/navigationHelper.ts | 14 +++++++++++ 7 files changed, 74 insertions(+), 9 deletions(-) create mode 100644 src/Browser/Avalonia.Browser/BrowserSystemNavigationManager.cs create mode 100644 src/Browser/Avalonia.Browser/Interop/NavigationHelper.cs create mode 100644 src/Browser/Avalonia.Browser/webapp/modules/avalonia/navigationHelper.ts 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.Controls/TopLevel.cs b/src/Avalonia.Controls/TopLevel.cs index 66c108b436..c0265e28b9 100644 --- a/src/Avalonia.Controls/TopLevel.cs +++ b/src/Avalonia.Controls/TopLevel.cs @@ -18,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 { @@ -223,20 +225,24 @@ namespace Avalonia.Controls }; } - var backKeyGesture = new KeyGesture(Key.Left, KeyModifiers.Alt); - _backGestureSubscription = _inputManager?.PreProcess.OfType().Subscribe(e => + _backGestureSubscription = _inputManager?.PreProcess.Subscribe(e => { bool backRequested = false; if (e is RawKeyEventArgs rawKeyEventArgs && rawKeyEventArgs.Type == RawKeyEventType.KeyDown) { - var keyEvent = new KeyEventArgs() + var keymap = AvaloniaLocator.Current.GetService()?.Back; + + if (keymap != null) { - KeyModifiers = (KeyModifiers)rawKeyEventArgs.Modifiers, - Key = rawKeyEventArgs.Key - }; + var keyEvent = new KeyEventArgs() + { + KeyModifiers = (KeyModifiers)rawKeyEventArgs.Modifiers, + Key = rawKeyEventArgs.Key + }; - backRequested = backKeyGesture.Matches(keyEvent); + backRequested = keymap.Any( key => key.Matches(keyEvent)); + } } else if(e is RawPointerEventArgs pointerEventArgs) { 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..90310fb0e1 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; @@ -234,5 +235,7 @@ namespace Avalonia.Browser public INativeControlHostImpl? NativeControlHost { get; } public IStorageProvider StorageProvider { get; } = new BrowserStorageProvider(); + + public ISystemNavigationManager SystemNavigationManager { get; } = new BrowserSystemNavigationManager(); } } 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/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/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(); + } + }; + } +}