Browse Source

Merge branch 'master' into colorpicker-updates-1

pull/8919/head
robloo 4 years ago
parent
commit
8031a0a315
  1. 3
      samples/RenderDemo/MainWindow.xaml
  2. 35
      samples/RenderDemo/Pages/SpringAnimationsPage.xaml
  3. 17
      samples/RenderDemo/Pages/SpringAnimationsPage.xaml.cs
  4. 80
      src/Avalonia.Base/Animation/Easings/SpringEasing.cs
  5. 143
      src/Avalonia.Base/Animation/Spring.cs
  6. 21
      src/Avalonia.Base/Animation/SpringTypeConverter.cs
  7. 2
      src/Avalonia.Base/Avalonia.Base.csproj
  8. 2
      src/Avalonia.Base/Data/Core/Plugins/ObservableStreamPlugin.cs
  9. 2
      src/Avalonia.Base/Input/GestureRecognizers/ScrollGestureRecognizer.cs
  10. 4
      src/Avalonia.Base/Input/KeyGesture.cs
  11. 1
      src/Avalonia.Base/Input/PointerEventArgs.cs
  12. 17
      src/Avalonia.Base/Input/PointerOverPreProcessor.cs
  13. 2
      src/Avalonia.Base/Layout/FlowLayoutAlgorithm.cs
  14. 6
      src/Avalonia.Base/Logging/TraceLogSink.cs
  15. 4
      src/Avalonia.Base/Media/BoxShadow.cs
  16. 6
      src/Avalonia.Base/Media/BoxShadows.cs
  17. 4
      src/Avalonia.Base/Media/Fonts/FamilyNameCollection.cs
  18. 4
      src/Avalonia.Base/Media/HslColor.cs
  19. 4
      src/Avalonia.Base/Media/HsvColor.cs
  20. 10
      src/Avalonia.Base/Media/TextTrimming.cs
  21. 2
      src/Avalonia.Base/Media/UnicodeRange.cs
  22. 31
      src/Avalonia.Base/Rendering/Composition/CompositingRenderer.cs
  23. 2
      src/Avalonia.Base/Rendering/Composition/CompositionTarget.cs
  24. 32
      src/Avalonia.Base/Rendering/Composition/Compositor.Factories.cs
  25. 134
      src/Avalonia.Base/Rendering/Composition/Compositor.cs
  26. 10
      src/Avalonia.Base/Rendering/Composition/Transport/Batch.cs
  27. 7
      src/Avalonia.Base/Styling/NthChildSelector.cs
  28. 6
      src/Avalonia.Base/Styling/PropertyEqualsSelector.cs
  29. 6
      src/Avalonia.Base/Styling/TypeNameAndClassSelector.cs
  30. 11
      src/Avalonia.Base/Threading/DispatcherPriority.cs
  31. 2
      src/Avalonia.Base/Utilities/IWeakSubscriber.cs
  32. 121
      src/Avalonia.Base/Utilities/SpringSolver.cs
  33. 68
      src/Avalonia.Base/Utilities/StringBuilderCache.cs
  34. 1
      src/Avalonia.Build.Tasks/Avalonia.Build.Tasks.csproj
  35. 6
      src/Avalonia.Controls.ColorPicker/Helpers/ColorHelper.cs
  36. 8
      src/Avalonia.Controls.DataGrid/DataGrid.cs
  37. 9
      src/Avalonia.Controls/Converters/PlatformKeyGestureConverter.cs
  38. 6
      src/Avalonia.Controls/Documents/InlineCollection.cs
  39. 2
      src/Avalonia.Controls/Window.cs
  40. 28
      src/Avalonia.DesignerSupport/Remote/HtmlTransport/webapp/build.js
  41. 3593
      src/Avalonia.DesignerSupport/Remote/HtmlTransport/webapp/package-lock.json
  42. 34
      src/Avalonia.DesignerSupport/Remote/HtmlTransport/webapp/package.json
  43. 13
      src/Avalonia.DesignerSupport/Remote/HtmlTransport/webapp/src/index.html
  44. 9
      src/Avalonia.DesignerSupport/Remote/HtmlTransport/webapp/tsconfig.json
  45. 116
      src/Avalonia.DesignerSupport/Remote/HtmlTransport/webapp/webpack.config.js
  46. 5
      src/Avalonia.Diagnostics/Diagnostics/VisualTreeDebug.cs
  47. 2
      src/Avalonia.MicroCom/MicroComProxyBase.cs
  48. 6
      src/Linux/Avalonia.LinuxFramebuffer/Input/LibInput/LibInputBackend.cs
  49. 16
      src/Linux/Avalonia.LinuxFramebuffer/Input/NullInput/NullInputBackend.cs
  50. 40
      src/Linux/Avalonia.LinuxFramebuffer/LinuxFramebufferPlatform.cs
  51. 3
      src/Markup/Avalonia.Markup.Xaml.Loader/Avalonia.Markup.Xaml.Loader.csproj
  52. 2
      src/Web/Avalonia.Web.Blazor/Avalonia.Web.Blazor.csproj
  53. 2
      src/Web/Avalonia.Web.Blazor/Interop/AvaloniaModule.cs
  54. 2
      src/Web/Avalonia.Web.Blazor/Interop/Storage/StorageProviderInterop.cs
  55. 16
      src/Web/Avalonia.Web.Blazor/webapp/build.js
  56. 5
      src/Web/Avalonia.Web.Blazor/webapp/modules/Avalonia.ts
  57. 5
      src/Web/Avalonia.Web.Blazor/webapp/modules/Avalonia/Avalonia.ts
  58. 1
      src/Web/Avalonia.Web.Blazor/webapp/modules/Storage.ts
  59. 9
      src/Web/Avalonia.Web.Blazor/webapp/package.json
  60. 6
      src/Web/Avalonia.Web.Blazor/webapp/tsconfig.json
  61. 40
      src/Web/Avalonia.Web.Blazor/webapp/webpack.config.js
  62. 6
      src/Windows/Avalonia.Win32/ClipboardFormats.cs
  63. 6
      src/Windows/Avalonia.Win32/Input/WindowsKeyboardDevice.cs
  64. 6
      src/Windows/Avalonia.Win32/OleDataObject.cs
  65. 1
      src/tools/Avalonia.Designer.HostApp/Avalonia.Designer.HostApp.csproj
  66. 4
      src/tools/DevGenerators/CompositionGenerator/Generator.cs
  67. 118
      tests/Avalonia.Base.UnitTests/Animation/SpringTests.cs
  68. 58
      tests/Avalonia.Base.UnitTests/Input/PointerOverTests.cs
  69. 2
      tests/Avalonia.Base.UnitTests/Input/PointerTests.cs
  70. 2
      tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextFormatterTests.cs

3
samples/RenderDemo/MainWindow.xaml

@ -39,6 +39,9 @@
<TabItem Header="Custom Animator">
<pages:CustomAnimatorPage />
</TabItem>
<TabItem Header="Spring Animation">
<pages:SpringAnimationsPage />
</TabItem>
<TabItem Header="Clipping">
<pages:ClippingPage />
</TabItem>

35
samples/RenderDemo/Pages/SpringAnimationsPage.xaml

@ -0,0 +1,35 @@
<UserControl
xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
x:Class="RenderDemo.Pages.SpringAnimationsPage"
MaxWidth="600">
<UserControl.Resources>
<SpringEasing x:Key="SpringEasing" Mass="1" Stiffness="2000" Damping="20" InitialVelocity="0" />
</UserControl.Resources>
<UserControl.Styles>
<Style Selector="Border.spring">
<Style.Animations>
<Animation Duration="0:0:0.900"
IterationCount="Infinite"
PlaybackDirection="Normal"
Easing="{DynamicResource SpringEasing}">
<KeyFrame Cue="0%" KeySpline="">
<Setter Property="TranslateTransform.X" Value="-300"/>
</KeyFrame>
<KeyFrame Cue="100%">
<Setter Property="TranslateTransform.X" Value="0" />
</KeyFrame>
</Animation>
</Style.Animations>
</Style>
</UserControl.Styles>
<Grid>
<Border Classes="spring" Background="Red" Width="50" Height="50">
<Border.RenderTransform>
<TransformGroup>
<TranslateTransform />
</TransformGroup>
</Border.RenderTransform>
</Border>
</Grid>
</UserControl>

17
samples/RenderDemo/Pages/SpringAnimationsPage.xaml.cs

@ -0,0 +1,17 @@
using Avalonia.Controls;
using Avalonia.Markup.Xaml;
namespace RenderDemo.Pages;
public class SpringAnimationsPage : UserControl
{
public SpringAnimationsPage()
{
InitializeComponent();
}
private void InitializeComponent()
{
AvaloniaXamlLoader.Load(this);
}
}

80
src/Avalonia.Base/Animation/Easings/SpringEasing.cs

@ -0,0 +1,80 @@
namespace Avalonia.Animation.Easings;
/// <summary>
/// Eases a <see cref="double"/> value using a user-defined spring formula.
/// </summary>
public class SpringEasing : Easing
{
private readonly Spring _internalSpring;
/// <summary>
/// The spring mass.
/// </summary>
public double Mass
{
get => _internalSpring.Mass;
set
{
_internalSpring.Mass = value;
}
}
/// <summary>
/// The spring stiffness.
/// </summary>
public double Stiffness
{
get => _internalSpring.Stiffness;
set
{
_internalSpring.Stiffness = value;
}
}
/// <summary>
/// The spring damping.
/// </summary>
public double Damping
{
get => _internalSpring.Damping;
set
{
_internalSpring.Damping = value;
}
}
/// <summary>
/// The spring initial velocity.
/// </summary>
public double InitialVelocity
{
get => _internalSpring.InitialVelocity;
set
{
_internalSpring.InitialVelocity = value;
}
}
public SpringEasing(double mass = 0d, double stiffness = 0d, double damping = 0d, double initialVelocity = 0d)
{
_internalSpring = new Spring();
Mass = mass;
Stiffness = stiffness;
Damping = damping;
InitialVelocity = initialVelocity;
}
public SpringEasing(Spring keySpline)
{
_internalSpring = keySpline;
}
public SpringEasing()
{
_internalSpring = new Spring();
}
/// <inheritdoc/>
public override double Ease(double progress) => _internalSpring.GetSpringProgress(progress);
}

143
src/Avalonia.Base/Animation/Spring.cs

@ -0,0 +1,143 @@
using System;
using System.ComponentModel;
using System.Globalization;
using Avalonia;
using Avalonia.Utilities;
namespace Avalonia.Animation;
/// <summary>
/// Determines how an animation is used based on spring formula.
/// </summary>
[TypeConverter(typeof(SpringTypeConverter))]
public class Spring
{
private SpringSolver _springSolver;
private double _mass;
private double _stiffness;
private double _damping;
private double _initialVelocity;
private bool _isDirty;
/// <summary>
/// Create a <see cref="Spring"/>.
/// </summary>
public Spring()
{
_mass = 0.0;
_stiffness = 0.0;
_damping = 0.0;
_initialVelocity = 0.0;
_isDirty = true;
}
/// <summary>
/// Create a <see cref="Spring"/> with the given parameters.
/// </summary>
/// <param name="mass">The spring mass.</param>
/// <param name="stiffness">The spring stiffness.</param>
/// <param name="damping">The spring damping.</param>
/// <param name="initialVelocity">The spring initial velocity.</param>
public Spring(double mass, double stiffness, double damping, double initialVelocity)
{
_mass = mass;
_stiffness = stiffness;
_damping = damping;
_initialVelocity = initialVelocity;
_isDirty = true;
}
/// <summary>
/// Parse a <see cref="Spring"/> from a string. The string needs to contain 4 values in it.
/// </summary>
/// <param name="value">string with 4 values in it</param>
/// <param name="culture">culture of the string</param>
/// <exception cref="FormatException">Thrown if the string does not have 4 values</exception>
/// <returns>A <see cref="Spring"/> with the appropriate values set</returns>
public static Spring Parse(string value, CultureInfo? culture)
{
if (culture is null)
{
culture = CultureInfo.InvariantCulture;
}
using var tokenizer = new StringTokenizer(value, culture, exceptionMessage: $"Invalid Spring string: \"{value}\".");
return new Spring(tokenizer.ReadDouble(), tokenizer.ReadDouble(), tokenizer.ReadDouble(), tokenizer.ReadDouble());
}
/// <summary>
/// The spring mass.
/// </summary>
public double Mass
{
get => _mass;
set
{
_mass = value;
_isDirty = true;
}
}
/// <summary>
/// The spring stiffness.
/// </summary>
public double Stiffness
{
get => _stiffness;
set
{
_stiffness = value;
_isDirty = true;
}
}
/// <summary>
/// The spring damping.
/// </summary>
public double Damping
{
get => _damping;
set
{
_damping = value;
_isDirty = true;
}
}
/// <summary>
/// The spring initial velocity.
/// </summary>
public double InitialVelocity
{
get => _initialVelocity;
set
{
_initialVelocity = value;
_isDirty = true;
}
}
/// <summary>
/// Calculates spring progress from a linear progress.
/// </summary>
/// <param name="linearProgress">the linear progress</param>
/// <returns>The spring progress</returns>
public double GetSpringProgress(double linearProgress)
{
if (_isDirty)
{
Build();
}
return _springSolver.Solve(linearProgress);
}
/// <summary>
/// Create cached spring solver.
/// </summary>
private void Build()
{
_springSolver = new SpringSolver(_mass, _stiffness, _damping, _initialVelocity);
_isDirty = false;
}
}

21
src/Avalonia.Base/Animation/SpringTypeConverter.cs

@ -0,0 +1,21 @@
using System;
using System.ComponentModel;
using System.Globalization;
namespace Avalonia.Animation;
/// <summary>
/// Converts string values to <see cref="Spring"/> values.
/// </summary>
public class SpringTypeConverter : TypeConverter
{
public override bool CanConvertFrom(ITypeDescriptorContext? context, Type sourceType)
{
return sourceType == typeof(string);
}
public override object ConvertFrom(ITypeDescriptorContext? context, CultureInfo? culture, object value)
{
return Spring.Parse((string)value, CultureInfo.InvariantCulture);
}
}

2
src/Avalonia.Base/Avalonia.Base.csproj

@ -35,8 +35,10 @@
<InternalsVisibleTo Include="Avalonia.Skia.RenderTests, PublicKey=$(AvaloniaPublicKey)" />
<InternalsVisibleTo Include="Avalonia.Skia.UnitTests, PublicKey=$(AvaloniaPublicKey)" />
<InternalsVisibleTo Include="Avalonia.UnitTests, PublicKey=$(AvaloniaPublicKey)" />
<InternalsVisibleTo Include="Avalonia.Win32, PublicKey=$(AvaloniaPublicKey)" />
<InternalsVisibleTo Include="Avalonia.Web.Blazor, PublicKey=$(AvaloniaPublicKey)" />
<InternalsVisibleTo Include="Avalonia.Dialogs, PublicKey=$(AvaloniaPublicKey)" />
<InternalsVisibleTo Include="Avalonia.Diagnostics, PublicKey=$(AvaloniaPublicKey)"/>
<InternalsVisibleTo Include="DynamicProxyGenAssembly2, PublicKey=0024000004800000940000000602000000240000525341310004000001000100c547cac37abd99c8db225ef2f6c8a3602f3b3606cc9891605d02baa56104f4cfc0734aa39b93bf7852f7d9266654753cc297e7d2edfe0bac1cdcf9f717241550e0a7b191195b7667bb4f64bcb8e2121380fd1d9d46ad2d92d2d15605093924cceaf74c4861eff62abf69b9291ed0a340e113be11e6a7d3113e92484cf7045cc7" />
</ItemGroup>

2
src/Avalonia.Base/Data/Core/Plugins/ObservableStreamPlugin.cs

@ -42,7 +42,7 @@ namespace Avalonia.Data.Core.Plugins
if (target is IObservable<object?> result)
{
return result;
};
}
// If the observable returns a value type then we need to call Observable.Select on it.
// First get the type of T in `IObservable<T>`.

2
src/Avalonia.Base/Input/GestureRecognizers/ScrollGestureRecognizer.cs

@ -71,7 +71,7 @@ namespace Avalonia.Input.GestureRecognizers
{
EndGesture();
_tracking = e.Pointer;
_gestureId = ScrollGestureEventArgs.GetNextFreeId();;
_gestureId = ScrollGestureEventArgs.GetNextFreeId();
_trackedRootPoint = e.GetPosition(_target);
}
}

4
src/Avalonia.Base/Input/KeyGesture.cs

@ -96,7 +96,7 @@ namespace Avalonia.Input
public override string ToString()
{
var s = new StringBuilder();
var s = StringBuilderCache.Acquire();
static void Plus(StringBuilder s)
{
@ -132,7 +132,7 @@ namespace Avalonia.Input
Plus(s);
s.Append(Key);
return s.ToString();
return StringBuilderCache.GetStringAndRelease(s);
}
public bool Matches(KeyEventArgs keyEvent) =>

1
src/Avalonia.Base/Input/PointerEventArgs.cs

@ -65,6 +65,7 @@ namespace Avalonia.Input
return default;
if (relativeTo == null)
return pt;
return pt * _rootVisual.TransformToVisual(relativeTo) ?? default;
}

17
src/Avalonia.Base/Input/PointerOverPreProcessor.cs

@ -68,27 +68,29 @@ namespace Avalonia.Input
if (dirtyRect.Contains(clientPoint))
{
SetPointerOver(pointer, _inputRoot, _inputRoot.InputHitTest(clientPoint), 0, clientPoint, PointerPointProperties.None, KeyModifiers.None);
var element = pointer.Captured ?? _inputRoot.InputHitTest(clientPoint);
SetPointerOver(pointer, _inputRoot, element, 0, clientPoint, PointerPointProperties.None, KeyModifiers.None);
}
else if (!_inputRoot.Bounds.Contains(clientPoint))
{
ClearPointerOver(pointer, _inputRoot, 0, new Point(-1, -1), PointerPointProperties.None, KeyModifiers.None);
ClearPointerOver(pointer, _inputRoot, 0, clientPoint, PointerPointProperties.None, KeyModifiers.None);
}
}
}
private void ClearPointerOver()
{
if (_lastPointer is (var pointer, var _))
if (_lastPointer is (var pointer, var position))
{
ClearPointerOver(pointer, _inputRoot, 0, new Point(-1, -1), PointerPointProperties.None, KeyModifiers.None);
var clientPoint = _inputRoot.PointToClient(position);
ClearPointerOver(pointer, _inputRoot, 0, clientPoint, PointerPointProperties.None, KeyModifiers.None);
}
_lastPointer = null;
_lastActivePointerDevice = null;
}
private void ClearPointerOver(IPointer pointer, IInputRoot root,
ulong timestamp, Point position, PointerPointProperties properties, KeyModifiers inputModifiers)
ulong timestamp, Point? position, PointerPointProperties properties, KeyModifiers inputModifiers)
{
var element = root.PointerOverElement;
if (element is null)
@ -96,11 +98,10 @@ namespace Avalonia.Input
return;
}
// Do not pass rootVisual, when we have unknown (negative) position,
// Do not pass rootVisual, when we have unknown position,
// so GetPosition won't return invalid values.
var hasPosition = position.X >= 0 && position.Y >= 0;
var e = new PointerEventArgs(InputElement.PointerExitedEvent, element, pointer,
hasPosition ? root : null, hasPosition ? position : default,
position.HasValue ? root : null, position.HasValue ? position.Value : default,
timestamp, properties, inputModifiers);
if (element != null && !element.IsAttachedToVisualTree)

2
src/Avalonia.Base/Layout/FlowLayoutAlgorithm.cs

@ -523,7 +523,7 @@ namespace Avalonia.Layout
{
firstRealizedElement = _elementManager.GetAt(0);
firstBounds = _elementManager.GetLayoutBoundsForRealizedIndex(0);
firstDataIndex = _elementManager.GetDataIndexFromRealizedRangeIndex(0);;
firstDataIndex = _elementManager.GetDataIndexFromRealizedRangeIndex(0);
int last = _elementManager.GetRealizedElementCount() - 1;
lastRealizedElement = _elementManager.GetAt(last);

6
src/Avalonia.Base/Logging/TraceLogSink.cs

@ -46,7 +46,7 @@ namespace Avalonia.Logging
object? source,
object?[]? values)
{
var result = new StringBuilder(template.Length);
var result = StringBuilderCache.Acquire(template.Length);
var r = new CharacterReader(template.AsSpan());
var i = 0;
@ -89,7 +89,7 @@ namespace Avalonia.Logging
result.Append(')');
}
return result.ToString();
return StringBuilderCache.GetStringAndRelease(result);
}
private static string Format(
@ -98,7 +98,7 @@ namespace Avalonia.Logging
object? source,
object?[] v)
{
var result = new StringBuilder(template.Length);
var result = StringBuilderCache.Acquire(template.Length);
var r = new CharacterReader(template.AsSpan());
var i = 0;

4
src/Avalonia.Base/Media/BoxShadow.cs

@ -80,7 +80,7 @@ namespace Avalonia.Media
public override string ToString()
{
var sb = new StringBuilder();
var sb = StringBuilderCache.Acquire();
if (IsEmpty)
{
@ -114,7 +114,7 @@ namespace Avalonia.Media
sb.AppendFormat(" {0}", Color.ToString());
return sb.ToString();
return StringBuilderCache.GetStringAndRelease(sb);
}
public static unsafe BoxShadow Parse(string s)

6
src/Avalonia.Base/Media/BoxShadows.cs

@ -1,7 +1,7 @@
using System;
using System.ComponentModel;
using System.Text;
using Avalonia.Animation.Animators;
using Avalonia.Utilities;
namespace Avalonia.Media
{
@ -45,7 +45,7 @@ namespace Avalonia.Media
public override string ToString()
{
var sb = new StringBuilder();
var sb = StringBuilderCache.Acquire();
if (Count == 0)
{
@ -57,7 +57,7 @@ namespace Avalonia.Media
sb.AppendFormat("{0} ", boxShadow.ToString());
}
return sb.ToString();
return StringBuilderCache.GetStringAndRelease(sb);
}

4
src/Avalonia.Base/Media/Fonts/FamilyNameCollection.cs

@ -77,7 +77,7 @@ namespace Avalonia.Media.Fonts
/// </returns>
public override string ToString()
{
var builder = new StringBuilder();
var builder = StringBuilderCache.Acquire();
for (var index = 0; index < Names.Count; index++)
{
@ -91,7 +91,7 @@ namespace Avalonia.Media.Fonts
builder.Append(", ");
}
return builder.ToString();
return StringBuilderCache.GetStringAndRelease(builder);
}
/// <summary>

4
src/Avalonia.Base/Media/HslColor.cs

@ -202,7 +202,7 @@ namespace Avalonia.Media
/// <inheritdoc/>
public override string ToString()
{
var sb = new StringBuilder();
var sb = StringBuilderCache.Acquire();
// Use a format similar to CSS. However:
// - To ensure precision is never lost, allow decimal places.
@ -225,7 +225,7 @@ namespace Avalonia.Media
sb.Append(A.ToString(CultureInfo.InvariantCulture));
sb.Append(')');
return sb.ToString();
return StringBuilderCache.GetStringAndRelease(sb);
}
/// <summary>

4
src/Avalonia.Base/Media/HsvColor.cs

@ -202,7 +202,7 @@ namespace Avalonia.Media
/// <inheritdoc/>
public override string ToString()
{
var sb = new StringBuilder();
var sb = StringBuilderCache.Acquire();
// Use a format similar to CSS. However:
// - To ensure precision is never lost, allow decimal places.
@ -225,7 +225,7 @@ namespace Avalonia.Media
sb.Append(A.ToString(CultureInfo.InvariantCulture));
sb.Append(')');
return sb.ToString();
return StringBuilderCache.GetStringAndRelease(sb);
}
/// <summary>

10
src/Avalonia.Base/Media/TextTrimming.cs

@ -8,7 +8,7 @@ namespace Avalonia.Media
/// </summary>
public abstract class TextTrimming
{
public static char s_defaultEllipsisChar = '\u2026';
internal const char DefaultEllipsisChar = '\u2026';
/// <summary>
/// Text is not trimmed.
@ -18,22 +18,22 @@ namespace Avalonia.Media
/// <summary>
/// Text is trimmed at a character boundary. An ellipsis (...) is drawn in place of remaining text.
/// </summary>
public static TextTrimming CharacterEllipsis { get; } = new TextTrailingTrimming(s_defaultEllipsisChar, false);
public static TextTrimming CharacterEllipsis { get; } = new TextTrailingTrimming(DefaultEllipsisChar, false);
/// <summary>
/// Text is trimmed at a word boundary. An ellipsis (...) is drawn in place of remaining text.
/// </summary>
public static TextTrimming WordEllipsis { get; } = new TextTrailingTrimming(s_defaultEllipsisChar, true);
public static TextTrimming WordEllipsis { get; } = new TextTrailingTrimming(DefaultEllipsisChar, true);
/// <summary>
/// Text is trimmed after a given prefix length. An ellipsis (...) is drawn in between prefix and suffix and represents remaining text.
/// </summary>
public static TextTrimming PrefixCharacterEllipsis { get; } = new TextLeadingPrefixTrimming(s_defaultEllipsisChar, 8);
public static TextTrimming PrefixCharacterEllipsis { get; } = new TextLeadingPrefixTrimming(DefaultEllipsisChar, 8);
/// <summary>
/// Text is trimmed at a character boundary starting from the beginning. An ellipsis (...) is drawn in place of remaining text.
/// </summary>
public static TextTrimming LeadingCharacterEllipsis { get; } = new TextLeadingPrefixTrimming(s_defaultEllipsisChar, 0);
public static TextTrimming LeadingCharacterEllipsis { get; } = new TextLeadingPrefixTrimming(DefaultEllipsisChar, 0);
/// <summary>
/// Creates properties that will be used for collapsing lines of text.

2
src/Avalonia.Base/Media/UnicodeRange.cs

@ -9,7 +9,7 @@ namespace Avalonia.Media
/// </summary>
public readonly struct UnicodeRange
{
public static UnicodeRange Default = Parse("0-10FFFD");
public readonly static UnicodeRange Default = Parse("0-10FFFD");
private readonly UnicodeRangeSegment _single;
private readonly IReadOnlyList<UnicodeRangeSegment>? _segments = null;

31
src/Avalonia.Base/Rendering/Composition/CompositingRenderer.cs

@ -3,6 +3,7 @@ using System.Collections.Generic;
using System.Linq;
using System.Numerics;
using System.Runtime.InteropServices;
using System.Threading.Tasks;
using Avalonia.Collections;
using Avalonia.Collections.Pooled;
using Avalonia.Media;
@ -27,8 +28,7 @@ public class CompositingRenderer : IRendererWithCompositor
private HashSet<Visual> _dirty = new();
private HashSet<Visual> _recalculateChildren = new();
private bool _queuedUpdate;
private Action _update;
private Action _invalidateScene;
private Action<Task> _update;
private bool _updating;
internal CompositionTarget CompositionTarget;
@ -47,7 +47,6 @@ public class CompositingRenderer : IRendererWithCompositor
CompositionTarget = compositor.CreateCompositionTarget(root.CreateRenderTarget);
CompositionTarget.Root = ((Visual)root!.VisualRoot!).AttachToCompositor(compositor);
_update = Update;
_invalidateScene = InvalidateScene;
}
/// <inheritdoc/>
@ -72,7 +71,7 @@ public class CompositingRenderer : IRendererWithCompositor
if(_queuedUpdate)
return;
_queuedUpdate = true;
_compositor.InvokeWhenReadyForNextCommit(_update);
_compositor.InvokeBeforeNextCommit(_update);
}
/// <inheritdoc/>
@ -151,12 +150,6 @@ public class CompositingRenderer : IRendererWithCompositor
if (compositionChildren.Count == visualChildren.Count)
{
bool mismatch = false;
if (v.HasNonUniformZIndexChildren)
{
}
if (sortedChildren != null)
for (var c = 0; c < visualChildren.Count; c++)
{
@ -202,9 +195,6 @@ public class CompositingRenderer : IRendererWithCompositor
}
}
private void InvalidateScene() =>
SceneInvalidated?.Invoke(this, new SceneInvalidatedEventArgs(_root, new Rect(_root.ClientSize)));
private void UpdateCore()
{
_queuedUpdate = false;
@ -252,10 +242,15 @@ public class CompositingRenderer : IRendererWithCompositor
_recalculateChildren.Clear();
CompositionTarget.Size = _root.ClientSize;
CompositionTarget.Scaling = _root.RenderScaling;
Compositor.InvokeOnNextCommit(_invalidateScene);
}
private void Update()
private async void TriggerSceneInvalidatedOnBatchCompletion(Task batchCompletion)
{
await batchCompletion;
SceneInvalidated?.Invoke(this, new SceneInvalidatedEventArgs(_root, new Rect(_root.ClientSize)));
}
private void Update(Task batchCompletion)
{
if(_updating)
return;
@ -276,10 +271,10 @@ public class CompositingRenderer : IRendererWithCompositor
public void Paint(Rect rect)
{
Update();
QueueUpdate();
CompositionTarget.RequestRedraw();
if(RenderOnlyOnRenderThread && Compositor.Loop.RunsInBackground)
Compositor.RequestCommitAsync().Wait();
Compositor.Commit().Wait();
else
CompositionTarget.ImmediateUIThreadRender();
}
@ -299,7 +294,7 @@ public class CompositingRenderer : IRendererWithCompositor
// Wait for the composition batch to be applied and rendered to guarantee that
// render target is not used anymore and can be safely disposed
if (Compositor.Loop.RunsInBackground)
_compositor.RequestCommitAsync().Wait();
_compositor.Commit().Wait();
}
/// <summary>

2
src/Avalonia.Base/Rendering/Composition/CompositionTarget.cs

@ -129,7 +129,7 @@ namespace Avalonia.Rendering.Composition
/// </summary>
internal void ImmediateUIThreadRender()
{
Compositor.RequestCommitAsync();
Compositor.Commit();
Compositor.Server.Render();
}
}

32
src/Avalonia.Base/Rendering/Composition/Compositor.Factories.cs

@ -0,0 +1,32 @@
using System;
using Avalonia.Platform;
using Avalonia.Rendering.Composition.Animations;
using Avalonia.Rendering.Composition.Server;
namespace Avalonia.Rendering.Composition;
public partial class Compositor
{
/// <summary>
/// Creates a new CompositionTarget
/// </summary>
/// <param name="renderTargetFactory">A factory method to create IRenderTarget to be called from the render thread</param>
/// <returns></returns>
public CompositionTarget CreateCompositionTarget(Func<IRenderTarget> renderTargetFactory)
{
return new CompositionTarget(this, new ServerCompositionTarget(_server, renderTargetFactory));
}
public CompositionContainerVisual CreateContainerVisual() => new(this, new ServerCompositionContainerVisual(_server));
public ExpressionAnimation CreateExpressionAnimation() => new ExpressionAnimation(this);
public ExpressionAnimation CreateExpressionAnimation(string expression) => new ExpressionAnimation(this)
{
Expression = expression
};
public ImplicitAnimationCollection CreateImplicitAnimationCollection() => new ImplicitAnimationCollection(this);
public CompositionAnimationGroup CreateAnimationGroup() => new CompositionAnimationGroup(this);
}

134
src/Avalonia.Base/Rendering/Composition/Compositor.cs

@ -10,6 +10,7 @@ using Avalonia.Rendering.Composition.Animations;
using Avalonia.Rendering.Composition.Server;
using Avalonia.Rendering.Composition.Transport;
using Avalonia.Threading;
using Avalonia.Utilities;
// Special license applies <see href="https://raw.githubusercontent.com/AvaloniaUI/Avalonia/master/src/Avalonia.Base/Rendering/Composition/License.md">License.md</see>
@ -24,16 +25,18 @@ namespace Avalonia.Rendering.Composition
{
internal IRenderLoop Loop { get; }
private ServerCompositor _server;
private bool _implicitBatchCommitQueued;
private Action _implicitBatchCommit;
private TaskCompletionSource<int>? _nextCommit;
private Action _commit;
private BatchStreamObjectPool<object?> _batchObjectPool = new();
private BatchStreamMemoryPool _batchMemoryPool = new();
private List<CompositionObject> _objectsForSerialization = new();
private Queue<Action<Task>> _invokeBeforeCommit = new();
internal ServerCompositor Server => _server;
private Task? _pendingBatch;
private readonly object _pendingBatchLock = new();
internal IEasing DefaultEasing { get; }
private List<Action>? _invokeOnNextCommit;
private readonly Stack<List<Action>> _invokeListPool = new();
private Task? _lastBatchCompleted;
/// <summary>
/// Creates a new compositor on a specified render loop that would use a particular GPU
@ -44,21 +47,15 @@ namespace Avalonia.Rendering.Composition
{
Loop = loop;
_server = new ServerCompositor(loop, gpu, _batchObjectPool, _batchMemoryPool);
_implicitBatchCommit = ImplicitBatchCommit;
_commit = () =>
{
Console.WriteLine("Dispatcher:Commit");
Commit();
};
DefaultEasing = new CubicBezierEasing(new Point(0.25f, 0.1f), new Point(0.25f, 1f));
}
/// <summary>
/// Creates a new CompositionTarget
/// </summary>
/// <param name="renderTargetFactory">A factory method to create IRenderTarget to be called from the render thread</param>
/// <returns></returns>
public CompositionTarget CreateCompositionTarget(Func<IRenderTarget> renderTargetFactory)
{
return new CompositionTarget(this, new ServerCompositionTarget(_server, renderTargetFactory));
}
/// <summary>
/// Requests pending changes in the composition objects to be serialized and sent to the render thread
/// </summary>
@ -66,7 +63,35 @@ namespace Avalonia.Rendering.Composition
public Task RequestCommitAsync()
{
Dispatcher.UIThread.VerifyAccess();
var batch = new Batch();
if (_nextCommit == null)
{
_nextCommit = new TaskCompletionSource<int>();
var pending = _pendingBatch;
if (pending != null)
{
pending.ContinueWith(_ =>
{
Dispatcher.UIThread.Post(_commit, DispatcherPriority.Composition);
});
}
else
Dispatcher.UIThread.Post(_commit, DispatcherPriority.Composition);
}
return _nextCommit.Task;
}
internal Task Commit()
{
Dispatcher.UIThread.VerifyAccess();
using var noPump = NonPumpingLockHelper.Use();
_nextCommit ??= new TaskCompletionSource<int>();
while (_invokeBeforeCommit.Count > 0)
_invokeBeforeCommit.Dequeue()(_nextCommit.Task);
var batch = new Batch(_nextCommit);
using (var writer = new BatchStreamWriter(batch.Changes, _batchMemoryPool, _batchObjectPool))
{
@ -84,71 +109,36 @@ namespace Avalonia.Rendering.Composition
batch.CommitedAt = Server.Clock.Elapsed;
_server.EnqueueBatch(batch);
if (_invokeOnNextCommit != null)
ScheduleCommitCallbacks(batch.Completed);
return _lastBatchCompleted = batch.Completed;
}
async void ScheduleCommitCallbacks(Task task)
{
var list = _invokeOnNextCommit;
_invokeOnNextCommit = null;
await task;
foreach (var i in list!)
i();
list.Clear();
_invokeListPool.Push(list);
}
public CompositionContainerVisual CreateContainerVisual() => new(this, new ServerCompositionContainerVisual(_server));
public ExpressionAnimation CreateExpressionAnimation() => new ExpressionAnimation(this);
public ExpressionAnimation CreateExpressionAnimation(string expression) => new ExpressionAnimation(this)
{
Expression = expression
};
public ImplicitAnimationCollection CreateImplicitAnimationCollection() => new ImplicitAnimationCollection(this);
public CompositionAnimationGroup CreateAnimationGroup() => new CompositionAnimationGroup(this);
private void QueueImplicitBatchCommit()
{
if(_implicitBatchCommitQueued)
return;
_implicitBatchCommitQueued = true;
Dispatcher.UIThread.Post(_implicitBatchCommit, DispatcherPriority.CompositionBatch);
}
private void ImplicitBatchCommit()
{
_implicitBatchCommitQueued = false;
RequestCommitAsync();
lock (_pendingBatchLock)
{
_pendingBatch = _nextCommit.Task;
_pendingBatch.ContinueWith(t =>
{
lock (_pendingBatchLock)
{
if (_pendingBatch == t)
_pendingBatch = null;
}
}, TaskContinuationOptions.ExecuteSynchronously);
_nextCommit = null;
return _pendingBatch;
}
}
internal void RegisterForSerialization(CompositionObject compositionObject)
{
Dispatcher.UIThread.VerifyAccess();
_objectsForSerialization.Add(compositionObject);
QueueImplicitBatchCommit();
}
internal void InvokeOnNextCommit(Action action)
{
_invokeOnNextCommit ??= _invokeListPool.Count > 0 ? _invokeListPool.Pop() : new();
_invokeOnNextCommit.Add(action);
RequestCommitAsync();
}
public void InvokeWhenReadyForNextCommit(Action action)
internal void InvokeBeforeNextCommit(Action<Task> action)
{
if (_lastBatchCompleted == null || _lastBatchCompleted.IsCompleted)
Dispatcher.UIThread.Post(action, DispatcherPriority.Composition);
else
_lastBatchCompleted.ContinueWith(
static (_, state) => Dispatcher.UIThread.Post((Action)state!, DispatcherPriority.Composition),
action);
Dispatcher.UIThread.VerifyAccess();
_invokeBeforeCommit.Enqueue(action);
RequestCommitAsync();
}
}
}

10
src/Avalonia.Base/Rendering/Composition/Transport/Batch.cs

@ -15,16 +15,19 @@ namespace Avalonia.Rendering.Composition.Transport
{
private static long _nextSequenceId = 1;
private static ConcurrentBag<BatchStreamData> _pool = new();
private readonly TaskCompletionSource<int>? _tcs;
public long SequenceId { get; }
public Batch()
public Batch(TaskCompletionSource<int>? tcs)
{
_tcs = tcs;
SequenceId = Interlocked.Increment(ref _nextSequenceId);
if (!_pool.TryTake(out var lst))
lst = new BatchStreamData();
Changes = lst;
}
private TaskCompletionSource<int> _tcs = new TaskCompletionSource<int>();
public BatchStreamData Changes { get; private set; }
public TimeSpan CommitedAt { get; set; }
@ -33,9 +36,8 @@ namespace Avalonia.Rendering.Composition.Transport
_pool.Add(Changes);
Changes = null!;
_tcs.TrySetResult(0);
_tcs?.TrySetResult(0);
}
public Task Completed => _tcs.Task;
}
}

7
src/Avalonia.Base/Styling/NthChildSelector.cs

@ -1,8 +1,8 @@
#nullable enable
using System;
using System.Text;
using Avalonia.LogicalTree;
using Avalonia.Styling.Activators;
using Avalonia.Utilities;
namespace Avalonia.Styling
{
@ -110,7 +110,8 @@ namespace Avalonia.Styling
public override string ToString()
{
var expectedCapacity = NthLastChildSelectorName.Length + 8;
var stringBuilder = new StringBuilder(_previous?.ToString(), expectedCapacity);
var stringBuilder = StringBuilderCache.Acquire(expectedCapacity);
stringBuilder.Append(_previous?.ToString());
stringBuilder.Append(':');
stringBuilder.Append(_reversed ? NthLastChildSelectorName : NthChildSelectorName);
@ -140,7 +141,7 @@ namespace Avalonia.Styling
stringBuilder.Append(')');
return stringBuilder.ToString();
return StringBuilderCache.GetStringAndRelease(stringBuilder);
}
}
}

6
src/Avalonia.Base/Styling/PropertyEqualsSelector.cs

@ -1,8 +1,8 @@
using System;
using System.ComponentModel;
using System.Globalization;
using System.Text;
using Avalonia.Styling.Activators;
using Avalonia.Utilities;
#nullable enable
@ -42,7 +42,7 @@ namespace Avalonia.Styling
{
if (_selectorString == null)
{
var builder = new StringBuilder();
var builder = StringBuilderCache.Acquire();
if (_previous != null)
{
@ -67,7 +67,7 @@ namespace Avalonia.Styling
builder.Append(_value ?? string.Empty);
builder.Append(']');
_selectorString = builder.ToString();
_selectorString = StringBuilderCache.GetStringAndRelease(builder);
}
return _selectorString;

6
src/Avalonia.Base/Styling/TypeNameAndClassSelector.cs

@ -1,8 +1,8 @@
using System;
using System.Collections.Generic;
using System.Text;
using Avalonia.Controls;
using Avalonia.Styling.Activators;
using Avalonia.Utilities;
#nullable enable
@ -145,7 +145,7 @@ namespace Avalonia.Styling
private string BuildSelectorString()
{
var builder = new StringBuilder();
var builder = StringBuilderCache.Acquire();
if (_previous != null)
{
@ -185,7 +185,7 @@ namespace Avalonia.Styling
}
}
return builder.ToString();
return StringBuilderCache.GetStringAndRelease(builder);
}
}
}

11
src/Avalonia.Base/Threading/DispatcherPriority.cs

@ -61,21 +61,16 @@ namespace Avalonia.Threading
/// The job will be processed with the same priority as render.
/// </summary>
public static readonly DispatcherPriority Render = new(5);
/// <summary>
/// The job will be processed with the same priority as composition batch commit.
/// </summary>
public static readonly DispatcherPriority CompositionBatch = new(6);
/// <summary>
/// The job will be processed with the same priority as composition updates.
/// </summary>
public static readonly DispatcherPriority Composition = new(7);
public static readonly DispatcherPriority Composition = new(6);
/// <summary>
/// The job will be processed with the same priority as render.
/// </summary>
public static readonly DispatcherPriority Layout = new(8);
public static readonly DispatcherPriority Layout = new(7);
/// <summary>
/// The job will be processed with the same priority as data binding.
@ -85,7 +80,7 @@ namespace Avalonia.Threading
/// <summary>
/// The job will be processed before other asynchronous operations.
/// </summary>
public static readonly DispatcherPriority Send = new(9);
public static readonly DispatcherPriority Send = new(8);
/// <summary>
/// Maximum possible priority

2
src/Avalonia.Base/Utilities/IWeakSubscriber.cs

@ -3,7 +3,7 @@ using System;
namespace Avalonia.Utilities
{
/// <summary>
/// Defines a listener to a event subscribed vis the <see cref="WeakSubscriptionManager"/>.
/// Defines a listener to a event subscribed vis the <see cref="WeakObservable"/>.
/// </summary>
/// <typeparam name="T">The type of the event arguments.</typeparam>
public interface IWeakSubscriber<T> where T : EventArgs

121
src/Avalonia.Base/Utilities/SpringSolver.cs

@ -0,0 +1,121 @@
// Ported from:
// https://svn.webkit.org/repository/webkit/trunk/Source/WebCore/platform/graphics/SpringSolver.h
/*
* Copyright (C) 2016 Apple Inc. All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions
* are met:
* 1. Redistributions of source code must retain the above copyright
* notice, this list of conditions and the following disclaimer.
* 2. Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in the
* documentation and/or other materials provided with the distribution.
*
* THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS''
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
* THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
* PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS
* BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
* CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF
* THE POSSIBILITY OF SUCH DAMAGE.
*/
using System;
namespace Avalonia.Utilities;
internal struct SpringSolver
{
private double m_w0;
private double m_zeta;
private double m_wd;
private double m_A;
private double m_B;
/// <summary>
///
/// </summary>
/// <param name="period">The time period.</param>
/// <param name="zeta">The damping ratio.</param>
/// <param name="initialVelocity"></param>
public SpringSolver(TimeSpan period, double zeta, double initialVelocity)
: this(
2 * Math.PI / period.TotalSeconds,
zeta,
initialVelocity)
{
// T is time period [s]
// T = (2*PI / sqrt(k)) * sqrt(m)
// ωn is natural frequency of the system [Hz] [1/s]
// ωn = 2*PI / T
}
/// <summary>
///
/// </summary>
/// <param name="m">The mass of the oscillating body.</param>
/// <param name="k">The stiffness of the oscillated body (spring constant).</param>
/// <param name="c">The actual damping.</param>
/// <param name="initialVelocity">The initial velocity.</param>
public SpringSolver(double m, double k, double c, double initialVelocity)
: this(
Math.Sqrt(k / m), // ωn
c / (2 * Math.Sqrt(k * m)), // c / Cc
initialVelocity)
{
// ωn is natural frequency of the system [Hz] [1/s]
// ωn = sqrt(k / m)
// Cc is critical damping coefficient
// Cc = 2 * Sqrt(k * m)
// Cc = 2 * m * wn
// Cc = 2 * m * Sqrt(k / m)
// ζ is damping ratio (Greek letter zeta)
// ζ = m_zeta = c / Cc
}
/// <summary>
///
/// </summary>
/// <param name="ωn">The the natural frequency of the system [rad/s].</param>
/// <param name="zeta">The damping ratio.</param>
/// <param name="initialVelocity"></param>
public SpringSolver(double ωn, double zeta, double initialVelocity)
{
m_w0 = ωn;
m_zeta = zeta;
if (m_zeta < 1) {
// Under-damped.
m_wd = m_w0 * Math.Sqrt(1 - m_zeta * m_zeta);
m_A = 1;
m_B = (m_zeta * m_w0 + -initialVelocity) / m_wd;
} else {
// Critically damped (ignoring over-damped case for now).
m_A = 1;
m_B = -initialVelocity + m_w0;
m_wd = 0;
}
}
public readonly double Solve(double t)
{
if (m_zeta < 1) {
// Under-damped
t = Math.Exp(-t * m_zeta * m_w0) * (m_A * Math.Cos(m_wd * t) + m_B * Math.Sin(m_wd * t));
} else {
// Critically damped (ignoring over-damped case for now).
t = (m_A + m_B * t) * Math.Exp(-t * m_w0);
}
// Map range from [1..0] to [0..1].
return 1 - t;
}
}

68
src/Avalonia.Base/Utilities/StringBuilderCache.cs

@ -0,0 +1,68 @@
// This file is imported from dotnet/runtime
// Source Link: https://github.com/dotnet/runtime/blob/e63d21947e734db2da5093510a6636b5b7fb45b5/src/libraries/Common/src/System/Text/StringBuilderCache.cs
// Commit: a9c5ead on Feb 10, 2021, https://github.com/dotnet/runtime/commit/a9c5eadd951dcba73167f72cc624eb790573663a
//
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
using System;
using System.Text;
namespace Avalonia.Utilities;
// <summary>Provide a cached reusable instance of stringbuilder per thread.</summary>
internal static class StringBuilderCache
{
// The value 360 was chosen in discussion with performance experts as a compromise between using
// as little memory per thread as possible and still covering a large part of short-lived
// StringBuilder creations on the startup path of VS designers.
internal const int MaxBuilderSize = 360;
private const int DefaultCapacity = 16; // == StringBuilder.DefaultCapacity
// WARNING: We allow diagnostic tools to directly inspect this member (t_cachedInstance).
// See https://github.com/dotnet/corert/blob/master/Documentation/design-docs/diagnostics/diagnostics-tools-contract.md for more details.
// Please do not change the type, the name, or the semantic usage of this member without understanding the implication for tools.
// Get in touch with the diagnostics team if you have questions.
[ThreadStatic]
private static StringBuilder? t_cachedInstance;
/// <summary>Get a StringBuilder for the specified capacity.</summary>
/// <remarks>If a StringBuilder of an appropriate size is cached, it will be returned and the cache emptied.</remarks>
public static StringBuilder Acquire(int capacity = DefaultCapacity)
{
if (capacity <= MaxBuilderSize)
{
StringBuilder? sb = t_cachedInstance;
if (sb != null)
{
// Avoid stringbuilder block fragmentation by getting a new StringBuilder
// when the requested size is larger than the current capacity
if (capacity <= sb.Capacity)
{
t_cachedInstance = null;
sb.Clear();
return sb;
}
}
}
return new StringBuilder(capacity);
}
/// <summary>Place the specified builder in the cache if it is not too big.</summary>
public static void Release(StringBuilder sb)
{
if (sb.Capacity <= MaxBuilderSize)
{
t_cachedInstance = sb;
}
}
/// <summary>ToString() the stringbuilder, Release it to the cache, and return the resulting string.</summary>
public static string GetStringAndRelease(StringBuilder sb)
{
string result = sb.ToString();
Release(sb);
return result;
}
}

1
src/Avalonia.Build.Tasks/Avalonia.Build.Tasks.csproj

@ -50,6 +50,7 @@
<Compile Include="../Avalonia.Base/Utilities/MathUtilities.cs">
<Link>Markup/%(RecursiveDir)%(FileName)%(Extension)</Link>
</Compile>
<Compile Include="..\Avalonia.Base\Utilities\StringBuilderCache.cs" Link="Utilities\StringBuilderCache.cs" />
<Compile Include="..\Markup\Avalonia.Markup\Markup\Parsers\ArgumentListParser.cs">
<Link>Markup/%(RecursiveDir)%(FileName)%(Extension)</Link>
</Compile>

6
src/Avalonia.Controls.ColorPicker/Helpers/ColorHelper.cs

@ -2,7 +2,7 @@
using System.Globalization;
using System.Collections.Generic;
using Avalonia.Media;
using System.Text;
using Avalonia.Utilities;
namespace Avalonia.Controls.Primitives
{
@ -113,7 +113,7 @@ namespace Avalonia.Controls.Primitives
// Cache results for next time as well
if (closestKnownColor != KnownColor.None)
{
StringBuilder sb = new StringBuilder();
var sb = StringBuilderCache.Acquire();
string name = closestKnownColor.ToString();
// Add spaces converting PascalCase to human-readable names
@ -128,7 +128,7 @@ namespace Avalonia.Controls.Primitives
sb.Append(name[i]);
}
string displayName = sb.ToString();
string displayName = StringBuilderCache.GetStringAndRelease(sb);
lock (cacheMutex)
{

8
src/Avalonia.Controls.DataGrid/DataGrid.cs

@ -6020,7 +6020,7 @@ namespace Avalonia.Controls
/// <returns>The formatted string.</returns>
private string FormatClipboardContent(DataGridRowClipboardEventArgs e)
{
var text = new StringBuilder();
var text = StringBuilderCache.Acquire();
var clipboardRowContent = e.ClipboardRowContent;
var numberOfItem = clipboardRowContent.Count;
for (int cellIndex = 0; cellIndex < numberOfItem; cellIndex++)
@ -6037,7 +6037,7 @@ namespace Avalonia.Controls
text.Append('\n');
}
}
return text.ToString();
return StringBuilderCache.GetStringAndRelease(text);
}
/// <summary>
@ -6052,7 +6052,7 @@ namespace Avalonia.Controls
if (ctrl && !shift && !alt && ClipboardCopyMode != DataGridClipboardCopyMode.None && SelectedItems.Count > 0)
{
StringBuilder textBuilder = new StringBuilder();
var textBuilder = StringBuilderCache.Acquire();
if (ClipboardCopyMode == DataGridClipboardCopyMode.IncludeHeader)
{
@ -6078,7 +6078,7 @@ namespace Avalonia.Controls
textBuilder.Append(FormatClipboardContent(itemArgs));
}
string text = textBuilder.ToString();
string text = StringBuilderCache.GetStringAndRelease(textBuilder);
if (!string.IsNullOrEmpty(text))
{

9
src/Avalonia.Controls/Converters/PlatformKeyGestureConverter.cs

@ -4,6 +4,7 @@ using System.Runtime.InteropServices;
using System.Text;
using Avalonia.Data.Converters;
using Avalonia.Input;
using Avalonia.Utilities;
namespace Avalonia.Controls.Converters
{
@ -62,7 +63,7 @@ namespace Avalonia.Controls.Converters
private static string ToString(KeyGesture gesture, string meta)
{
var s = new StringBuilder();
var s = StringBuilderCache.Acquire();
static void Plus(StringBuilder s)
{
@ -98,12 +99,12 @@ namespace Avalonia.Controls.Converters
Plus(s);
s.Append(ToString(gesture.Key));
return s.ToString();
return StringBuilderCache.GetStringAndRelease(s);
}
private static string ToOSXString(KeyGesture gesture)
{
var s = new StringBuilder();
var s = StringBuilderCache.Acquire();
if (gesture.KeyModifiers.HasAllFlags(KeyModifiers.Control))
{
@ -127,7 +128,7 @@ namespace Avalonia.Controls.Converters
s.Append(ToOSXString(gesture.Key));
return s.ToString();
return StringBuilderCache.GetStringAndRelease(s);
}
private static string ToString(Key key)

6
src/Avalonia.Controls/Documents/InlineCollection.cs

@ -1,8 +1,8 @@
using System;
using System.Text;
using Avalonia.Collections;
using Avalonia.LogicalTree;
using Avalonia.Metadata;
using Avalonia.Utilities;
namespace Avalonia.Controls.Documents
{
@ -70,14 +70,14 @@ namespace Avalonia.Controls.Documents
{
get
{
var builder = new StringBuilder();
var builder = StringBuilderCache.Acquire();
foreach (var inline in this)
{
inline.AppendText(builder);
}
return builder.ToString();
return StringBuilderCache.GetStringAndRelease(builder);
}
}

2
src/Avalonia.Controls/Window.cs

@ -378,8 +378,6 @@ namespace Avalonia.Controls
/// <summary>
/// Enables or disables resizing of the window.
/// Note that if <see cref="HasSystemDecorations"/> is set to False then this property
/// has no effect and should be treated as a recommendation for the user setting HasSystemDecorations.
/// </summary>
public bool CanResize
{

28
src/Avalonia.DesignerSupport/Remote/HtmlTransport/webapp/build.js

@ -0,0 +1,28 @@
import { build } from 'esbuild';
import htmlPlugin from '@chialab/esbuild-plugin-html';
import { compress } from 'esbuild-plugin-compress';
build({
entryPoints: ['src/index.html'],
chunkNames: '[name]-[hash]',
logLevel: "warning",
outdir: "./build",
bundle: true,
treeShaking: true,
minify: true,
write: false,
format: "esm",
target: "es6",
platform: "browser",
sourcemap: "linked",
loader: {".ts": "tsx"},
plugins: [
htmlPlugin(),
compress({
brotli: false,
gzip: true
})
]
})
.then(() => console.log("⚡ Done"))
.catch(() => process.exit(1));

3593
src/Avalonia.DesignerSupport/Remote/HtmlTransport/webapp/package-lock.json

File diff suppressed because it is too large

34
src/Avalonia.DesignerSupport/Remote/HtmlTransport/webapp/package.json

@ -1,34 +1,16 @@
{
"name": "simple",
"version": "1.0.0",
"description": "",
"main": "index.js",
"name": "previewer",
"scripts": {
"webpack-ver": "cross-env NODE_ENV=production webpack --version",
"dist": "cross-env NODE_ENV=production webpack",
"watch": "cross-env NODE_ENV=development webpack --watch"
"predist": "tsc -noEmit",
"dist": "node build.js"
},
"author": "",
"license": "ISC",
"devDependencies": {
"clean-webpack-plugin": "^4.0.0",
"compression-webpack-plugin": "^9.2.0",
"cross-env": "^7.0.3",
"css-loader": "^6.5.1",
"file-loader": "^6.2.0",
"html-webpack-plugin": "^5.5.0",
"mini-css-extract-plugin": "^2.5.3",
"source-map-loader": "^3.0.1",
"style-loader": "^3.3.1",
"to-string-loader": "^1.2.0",
"ts-loader": "^9.2.6",
"tsconfig-paths-webpack-plugin": "^3.5.2",
"typescript": "^4.5.5",
"url-loader": "^4.1.1",
"webpack": "^5.68.0",
"webpack-cli": "^4.9.2",
"webpack-livereload-plugin": "^3.0.2"
"@chialab/esbuild-plugin-html": "^0.17.0",
"esbuild": "^0.14.54",
"esbuild-plugin-compress": "^0.4.0",
"typescript": "^4.7.4"
},
"type": "module",
"dependencies": {
"@types/react": "^17.0.38",
"@types/react-dom": "^17.0.11",

13
src/Avalonia.DesignerSupport/Remote/HtmlTransport/webapp/src/index.html

@ -1,14 +1,15 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Avalonia XAML previewer web edition</title>
</head>
<body>
<div id="app">
<center>Loading...</center>
</div>
<noscript>JavaScript is required</noscript>
<div id="app">
<center>Loading...</center>
</div>
<noscript>JavaScript is required</noscript>
<script src="index.tsx"></script>
</body>
</html>
</html>

9
src/Avalonia.DesignerSupport/Remote/HtmlTransport/webapp/tsconfig.json

@ -2,7 +2,7 @@
"compilerOptions": {
"outDir": "build/dist",
"module": "esnext",
"target": "es5",
"target": "es6",
"lib": ["es6", "dom"],
"sourceMap": true,
"allowJs": true,
@ -25,11 +25,6 @@
"include": ["./src/**/*"],
"exclude": [
"node_modules",
"build",
"scripts",
"acceptance-tests",
"webpack",
"jest",
"src/setupTests.ts"
"build"
]
}

116
src/Avalonia.DesignerSupport/Remote/HtmlTransport/webapp/webpack.config.js

@ -1,116 +0,0 @@
const webpack = require('webpack');
const path = require('path');
const LiveReloadPlugin = require('webpack-livereload-plugin');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const CompressionPlugin = require('compression-webpack-plugin');
const { CleanWebpackPlugin } = require('clean-webpack-plugin');
const MiniCssExtractPlugin = require("mini-css-extract-plugin");
const TsconfigPathsPlugin = require('tsconfig-paths-webpack-plugin');
const prod = process.env.NODE_ENV == 'production';
class Printer {
apply(compiler) {
compiler.hooks.afterEmit.tap("Printer", ()=> console.log("Build completed at " + new Date().toString()));
compiler.hooks.watchRun.tap("Printer", ()=> console.log("Watch triggered at " + new Date().toString()));
}
}
const config = {
stats: {
errorDetails: true
},
entry: {
bundle: './src/index.tsx'
},
output: {
path: path.resolve(__dirname, 'build'),
publicPath: '/',
filename: '[name].[chunkhash].js'
},
performance: { hints: false },
mode: prod ? "production" : "development",
module: {
rules: [
{
enforce: "pre",
test: /\.js$/,
loader: "source-map-loader",
exclude: [
path.resolve(__dirname, 'node_modules/mobx-state-router')
]
},
{
"oneOf": [
{
test: /\.(ts|tsx)$/,
exclude: /node_modules/,
use: 'ts-loader'
},
{
test: /\.css$/,
use: [
MiniCssExtractPlugin.loader,
'css-loader'
]
},
{
test: /\.(jpg|png)$/,
use: {
loader: "url-loader",
options: {
limit: 25000,
},
},
},
{
test: /.(ttf|otf|eot|svg|woff(2)?)(\?[a-z0-9]+)?$/,
use: [{
loader: 'file-loader',
options: {
name: '[name].[ext]',
outputPath: 'fonts/', // where the fonts will go
}
}]
},
{
loader: require.resolve('file-loader'),
exclude: [/\.(js|jsx|mjs|tsx|ts)$/, /\.html$/, /\.json$/],
options: {
name: 'assets/[name].[hash:8].[ext]',
},
}]
},
]
},
devtool: "source-map",
resolve: {
modules: [path.resolve(__dirname, 'node_modules')],
plugins: [new TsconfigPathsPlugin({ configFile: "./tsconfig.json", logLevel: 'info' })],
extensions: ['.ts', '.tsx', '.js', '.json'],
alias: {
'src': path.resolve(__dirname, 'src')
}
},
plugins:
[
new Printer(),
new CleanWebpackPlugin({
cleanAfterEveryBuildPatterns: ['dist']
}),
new MiniCssExtractPlugin({
filename: "[name].[chunkhash]h" +
".css",
chunkFilename: "[id].[chunkhash].css"
}),
new LiveReloadPlugin({appendScriptTag: !prod}),
new HtmlWebpackPlugin({
template: path.resolve(__dirname, './src/index.html'),
filename: 'index.html' //relative to root of the application
}),
new CompressionPlugin({
test: /(\?.*)?$/i
})
]
};
module.exports = config;

5
src/Avalonia.Diagnostics/Diagnostics/VisualTreeDebug.cs

@ -2,6 +2,7 @@ using System;
using System.Text;
using Avalonia.Controls;
using Avalonia.Data;
using Avalonia.Utilities;
using Avalonia.VisualTree;
namespace Avalonia.Diagnostics
@ -10,9 +11,9 @@ namespace Avalonia.Diagnostics
{
public static string PrintVisualTree(IVisual visual)
{
StringBuilder result = new StringBuilder();
var result = StringBuilderCache.Acquire();
PrintVisualTree(visual, result, 0);
return result.ToString();
return StringBuilderCache.GetStringAndRelease(result);
}
private static void PrintVisualTree(IVisual visual, StringBuilder builder, int indent)

2
src/Avalonia.MicroCom/MicroComProxyBase.cs

@ -65,7 +65,7 @@ namespace Avalonia.MicroCom
protected virtual void Dispose(bool disposing)
{
if(_nativePointer == null)
if(_nativePointer == IntPtr.Zero)
return;
if (_ownsHandle)
{

6
src/Linux/Avalonia.LinuxFramebuffer/Input/LibInput/LibInputBackend.cs

@ -1,11 +1,9 @@
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.IO;
using System.Threading;
using Avalonia.Input;
using Avalonia.Input.Raw;
using Avalonia.Threading;
using static Avalonia.LinuxFramebuffer.Input.LibInput.LibInputNativeUnsafeMethods;
namespace Avalonia.LinuxFramebuffer.Input.LibInput
{
@ -29,8 +27,6 @@ namespace Avalonia.LinuxFramebuffer.Input.LibInput
new Thread(()=>InputThread(ctx)).Start();
}
private unsafe void InputThread(IntPtr ctx)
{
var fd = libinput_get_fd(ctx);
@ -143,8 +139,6 @@ namespace Avalonia.LinuxFramebuffer.Input.LibInput
}
}
public void Initialize(IScreenInfoProvider screen, Action<RawInputEventArgs> onInput)
{

16
src/Linux/Avalonia.LinuxFramebuffer/Input/NullInput/NullInputBackend.cs

@ -0,0 +1,16 @@
using System;
using Avalonia.Input;
using Avalonia.Input.Raw;
namespace Avalonia.LinuxFramebuffer.Input.NullInput;
public class NullInputBackend : IInputBackend
{
public void Initialize(IScreenInfoProvider screen, Action<RawInputEventArgs> onInput)
{
}
public void SetInputRoot(IInputRoot root)
{
}
}

40
src/Linux/Avalonia.LinuxFramebuffer/LinuxFramebufferPlatform.cs

@ -42,9 +42,9 @@ namespace Avalonia.LinuxFramebuffer
Threading = new InternalPlatformThreadingInterface();
if (_fb is IGlOutputBackend gl)
AvaloniaLocator.CurrentMutable.Bind<IPlatformOpenGlInterface>().ToConstant(gl.PlatformOpenGlInterface);
var opts = AvaloniaLocator.Current.GetService<LinuxFramebufferPlatformOptions>() ?? new LinuxFramebufferPlatformOptions();
AvaloniaLocator.CurrentMutable
.Bind<IPlatformThreadingInterface>().ToConstant(Threading)
.Bind<IRenderTimer>().ToConstant(new DefaultRenderTimer(opts.Fps))
@ -59,12 +59,12 @@ namespace Avalonia.LinuxFramebuffer
AvaloniaLocator.Current.GetService<IPlatformOpenGlInterface>());
}
internal static LinuxFramebufferLifetime Initialize<T>(T builder, IOutputBackend outputBackend) where T : AppBuilderBase<T>, new()
internal static LinuxFramebufferLifetime Initialize<T>(T builder, IOutputBackend outputBackend, IInputBackend inputBackend) where T : AppBuilderBase<T>, new()
{
var platform = new LinuxFramebufferPlatform(outputBackend);
builder.UseSkia().UseWindowingSubsystem(platform.Initialize, "fbdev");
return new LinuxFramebufferLifetime(platform._fb);
return new LinuxFramebufferLifetime(platform._fb, inputBackend);
}
}
@ -80,13 +80,13 @@ namespace Avalonia.LinuxFramebuffer
{
_fb = fb;
}
public LinuxFramebufferLifetime(IOutputBackend fb, IInputBackend input)
{
_fb = fb;
_inputBackend = input;
}
public Control MainView
{
get => (Control)_topLevel?.Content;
@ -126,7 +126,7 @@ namespace Avalonia.LinuxFramebuffer
{
Startup?.Invoke(this, new ControlledApplicationLifetimeStartupEventArgs(args));
}
public void Shutdown(int exitCode)
{
ExitCode = exitCode;
@ -140,22 +140,22 @@ namespace Avalonia.LinuxFramebuffer
public static class LinuxFramebufferPlatformExtensions
{
public static int StartLinuxFbDev<T>(this T builder, string[] args, string fbdev = null, double scaling = 1)
public static int StartLinuxFbDev<T>(this T builder, string[] args, string fbdev = null, double scaling = 1, IInputBackend inputBackend = default)
where T : AppBuilderBase<T>, new() =>
StartLinuxDirect(builder, args, new FbdevOutput(fileName: fbdev, format: null) { Scaling = scaling });
public static int StartLinuxFbDev<T>(this T builder, string[] args, string fbdev, PixelFormat? format, double scaling)
StartLinuxDirect(builder, args, new FbdevOutput(fileName: fbdev, format: null) { Scaling = scaling }, inputBackend);
public static int StartLinuxFbDev<T>(this T builder, string[] args, string fbdev, PixelFormat? format, double scaling, IInputBackend inputBackend = default)
where T : AppBuilderBase<T>, new() =>
StartLinuxDirect(builder, args, new FbdevOutput(fileName: fbdev, format: format) { Scaling = scaling });
public static int StartLinuxDrm<T>(this T builder, string[] args, string card = null, double scaling = 1)
where T : AppBuilderBase<T>, new() => StartLinuxDirect(builder, args, new DrmOutput(card) {Scaling = scaling});
public static int StartLinuxDrm<T>(this T builder, string[] args, string card = null, bool connectorsForceProbe = false, [CanBeNull] DrmOutputOptions options = null)
where T : AppBuilderBase<T>, new() => StartLinuxDirect(builder, args, new DrmOutput(card, connectorsForceProbe, options));
public static int StartLinuxDirect<T>(this T builder, string[] args, IOutputBackend backend)
StartLinuxDirect(builder, args, new FbdevOutput(fileName: fbdev, format: format) { Scaling = scaling }, inputBackend);
public static int StartLinuxDrm<T>(this T builder, string[] args, string card = null, double scaling = 1, IInputBackend inputBackend = default)
where T : AppBuilderBase<T>, new() => StartLinuxDirect(builder, args, new DrmOutput(card) { Scaling = scaling }, inputBackend);
public static int StartLinuxDrm<T>(this T builder, string[] args, string card = null, bool connectorsForceProbe = false, [CanBeNull] DrmOutputOptions options = null, IInputBackend inputBackend = default)
where T : AppBuilderBase<T>, new() => StartLinuxDirect(builder, args, new DrmOutput(card, connectorsForceProbe, options), inputBackend);
public static int StartLinuxDirect<T>(this T builder, string[] args, IOutputBackend outputBackend, IInputBackend inputBackend = default)
where T : AppBuilderBase<T>, new()
{
var lifetime = LinuxFramebufferPlatform.Initialize(builder, backend);
var lifetime = LinuxFramebufferPlatform.Initialize(builder, outputBackend, inputBackend);
builder.SetupWithLifetime(lifetime);
lifetime.Start(args);
builder.Instance.Run(lifetime.Token);

3
src/Markup/Avalonia.Markup.Xaml.Loader/Avalonia.Markup.Xaml.Loader.csproj

@ -7,6 +7,9 @@
<DefineConstants>$(DefineConstants);XAMLX_INTERNAL</DefineConstants>
</PropertyGroup>
<Import Project="IncludeXamlIlSre.props" />
<ItemGroup>
<Compile Include="..\..\Avalonia.Base\Utilities\StringBuilderCache.cs" Link="Utilities\StringBuilderCache.cs" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="System.Reflection.Emit" Version="4.3.0" />
</ItemGroup>

2
src/Web/Avalonia.Web.Blazor/Avalonia.Web.Blazor.csproj

@ -31,8 +31,6 @@
<ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.Components.Web" Version="6.0.8" />
<PackageReference Include="Microsoft.TypeScript.MSBuild" Version="4.7.4" PrivateAssets="all" />
<PackageReference Include="Microsoft.AspNetCore.Components.Web" Version="6.0.0" />
</ItemGroup>
<ItemGroup>

2
src/Web/Avalonia.Web.Blazor/Interop/AvaloniaModule.cs

@ -4,7 +4,7 @@ namespace Avalonia.Web.Blazor.Interop
{
internal class AvaloniaModule : JSModuleInterop
{
private AvaloniaModule(IJSRuntime js) : base(js, "./_content/Avalonia.Web.Blazor/avalonia.js")
private AvaloniaModule(IJSRuntime js) : base(js, "./_content/Avalonia.Web.Blazor/Avalonia.js")
{
}

2
src/Web/Avalonia.Web.Blazor/Interop/Storage/StorageProviderInterop.cs

@ -12,7 +12,7 @@ namespace Avalonia.Web.Blazor.Interop.Storage
internal class StorageProviderInterop : JSModuleInterop, IStorageProvider
{
private const string JsFilename = "./_content/Avalonia.Web.Blazor/avaloniaStorage.js";
private const string JsFilename = "./_content/Avalonia.Web.Blazor/Storage.js";
private const string PickerCancelMessage = "The user aborted a request";
public static async Task<StorageProviderInterop> ImportAsync(IJSRuntime js)

16
src/Web/Avalonia.Web.Blazor/webapp/build.js

@ -0,0 +1,16 @@
require("esbuild").build({
entryPoints: [
"./modules/Avalonia.ts",
"./modules/Storage.ts"
],
outdir: "../wwwroot",
bundle: true,
minify: true,
format: "esm",
target: "es2016",
platform: "browser",
sourcemap: "linked",
loader: {".ts": "ts"}
})
.then(() => console.log("⚡ Done"))
.catch(() => process.exit(1));

5
src/Web/Avalonia.Web.Blazor/webapp/modules/Avalonia.ts

@ -0,0 +1,5 @@
export { DpiWatcher } from "./Avalonia/DpiWatcher"
export { InputHelper } from "./Avalonia/InputHelper"
export { NativeControlHost } from "./Avalonia/NativeControlHost"
export { SizeWatcher } from "./Avalonia/SizeWatcher"
export { SKHtmlCanvas } from "./Avalonia/SKHtmlCanvas"

5
src/Web/Avalonia.Web.Blazor/webapp/modules/Avalonia/Avalonia.ts

@ -1,5 +0,0 @@
export { DpiWatcher } from "./DpiWatcher"
export { InputHelper } from "./InputHelper"
export { NativeControlHost } from "./NativeControlHost"
export { SizeWatcher } from "./SizeWatcher"
export { SKHtmlCanvas } from "./SKHtmlCanvas"

1
src/Web/Avalonia.Web.Blazor/webapp/modules/Storage.ts

@ -0,0 +1 @@
export { StorageProvider } from "./Storage/StorageProvider"

9
src/Web/Avalonia.Web.Blazor/webapp/package.json

@ -1,16 +1,13 @@
{
"name": "avalonia.web",
"scripts": {
"dist": "cross-env NODE_ENV=production webpack",
"build": "cross-env NODE_ENV=development webpack"
"prebuild": "tsc -noEmit",
"build": "node build.js"
},
"devDependencies": {
"@types/emscripten": "^1.39.6",
"@types/wicg-file-system-access": "^2020.9.5",
"cross-env": "^7.0.3",
"ts-loader": "^9.3.1",
"typescript": "^4.7.4",
"webpack": "^5.73.0",
"webpack-cli": "^4.10.0"
"esbuild": "^0.15.7"
}
}

6
src/Web/Avalonia.Web.Blazor/webapp/tsconfig.json

@ -1,14 +1,14 @@
{
"compilerOptions": {
"target": "ES6",
"target": "es2016",
"strict": true,
"sourceMap": true,
"outDir": "../wwwroot",
"removeComments": true,
"noEmitOnError": true,
"isolatedModules": true, // we need it for esbuild
"lib": [
"dom",
"ES6",
"es2016",
"esnext.asynciterable"
]
},

40
src/Web/Avalonia.Web.Blazor/webapp/webpack.config.js

@ -1,40 +0,0 @@
const path = require('path');
const prod = process.env.NODE_ENV == 'production';
module.exports = {
mode: prod ? "production" : "development",
devtool: 'source-map',
target: ["web", "es2020"],
entry: {
avalonia: './modules/Avalonia/Avalonia.ts',
avaloniaStorage: {
import: './modules/Storage/StorageProvider.ts',
dependOn: 'avalonia',
}
},
output: {
filename: '[name].js',
path: path.resolve(__dirname, '../wwwroot'),
library: {
type: 'module',
},
},
module: {
rules: [
{
test: /\.tsx?$/,
use: 'ts-loader',
exclude: /node_modules/,
},
],
},
resolve: {
extensions: ['.ts', '.js'],
},
optimization: {
minimize: false
},
experiments: {
outputModule: true,
}
};

6
src/Windows/Avalonia.Win32/ClipboardFormats.cs

@ -2,9 +2,9 @@
using System.Collections.Generic;
using System.ComponentModel;
using System.Linq;
using System.Text;
using Avalonia.Input;
using Avalonia.Win32.Interop;
using Avalonia.Utilities;
namespace Avalonia.Win32
{
@ -35,9 +35,9 @@ namespace Avalonia.Win32
private static string QueryFormatName(ushort format)
{
StringBuilder sb = new StringBuilder(MAX_FORMAT_NAME_LENGTH);
var sb = StringBuilderCache.Acquire(MAX_FORMAT_NAME_LENGTH);
if (UnmanagedMethods.GetClipboardFormatName(format, sb, sb.Capacity) > 0)
return sb.ToString();
return StringBuilderCache.GetStringAndRelease(sb);
return null;
}

6
src/Windows/Avalonia.Win32/Input/WindowsKeyboardDevice.cs

@ -1,6 +1,6 @@
using System.Text;
using Avalonia.Controls;
using Avalonia.Input;
using Avalonia.Utilities;
using Avalonia.Win32.Interop;
namespace Avalonia.Win32.Input
@ -49,7 +49,7 @@ namespace Avalonia.Win32.Input
public string StringFromVirtualKey(uint virtualKey)
{
StringBuilder result = new StringBuilder(256);
var result = StringBuilderCache.Acquire(256);
int length = UnmanagedMethods.ToUnicode(
virtualKey,
0,
@ -57,7 +57,7 @@ namespace Avalonia.Win32.Input
result,
256,
0);
return result.ToString();
return StringBuilderCache.GetStringAndRelease(result);
}
private void UpdateKeyStates()

6
src/Windows/Avalonia.Win32/OleDataObject.cs

@ -6,9 +6,9 @@ using System.Linq;
using System.Runtime.InteropServices;
using System.Runtime.InteropServices.ComTypes;
using System.Runtime.Serialization.Formatters.Binary;
using System.Text;
using Avalonia.Input;
using Avalonia.MicroCom;
using Avalonia.Utilities;
using Avalonia.Win32.Interop;
using IDataObject = Avalonia.Input.IDataObject;
@ -103,11 +103,11 @@ namespace Avalonia.Win32
for (int i = 0; i < fileCount; i++)
{
int pathLen = UnmanagedMethods.DragQueryFile(hGlobal, i, null, 0);
StringBuilder sb = new StringBuilder(pathLen+1);
var sb = StringBuilderCache.Acquire(pathLen+1);
if (UnmanagedMethods.DragQueryFile(hGlobal, i, sb, sb.Capacity) == pathLen)
{
files.Add(sb.ToString());
files.Add(StringBuilderCache.GetStringAndRelease(sb));
}
}
}

1
src/tools/Avalonia.Designer.HostApp/Avalonia.Designer.HostApp.csproj

@ -16,6 +16,7 @@
<ItemGroup>
<Compile Include="..\..\..\src\Markup\Avalonia.Markup.Xaml.Loader\CompilerExtensions\**\*.cs" />
<Compile Include="..\..\..\src\Markup\Avalonia.Markup.Xaml.Loader\AvaloniaXamlIlRuntimeCompiler.cs" />
<Compile Include="..\..\Avalonia.Base\Utilities\StringBuilderCache.cs" Link="Utilities\StringBuilderCache.cs" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Avalonia.Angle.Windows.Natives" Version="2.1.0.2020091801" />

4
src/tools/DevGenerators/CompositionGenerator/Generator.cs

@ -133,10 +133,6 @@ namespace Avalonia.SourceGenerator.CompositionGenerator
MethodDeclaration(ParseTypeName("void"), "Initialize")
.AddModifiers(SyntaxKind.PartialKeyword).WithSemicolonToken(Semicolon()));
var changesVarName = "c";
var changesVar = IdentifierName(changesVarName);
server = server.AddMembers(
MethodDeclaration(ParseTypeName("void"), "DeserializeChangesExtra")
.AddParameterListParameters(Parameter(Identifier("c")).WithType(ParseTypeName("BatchStreamReader")))

118
tests/Avalonia.Base.UnitTests/Animation/SpringTests.cs

@ -0,0 +1,118 @@
using System;
using Avalonia.Animation;
using Avalonia.Animation.Easings;
using Avalonia.Controls.Shapes;
using Avalonia.Media;
using Avalonia.Styling;
using Xunit;
namespace Avalonia.Base.UnitTests.Animation;
public class SpringTests
{
[Theory]
[InlineData("1,2 3,4")]
public void Can_Parse_Spring_Via_TypeConverter(string input)
{
var conv = new SpringTypeConverter();
var spring = (Spring)conv.ConvertFrom(input);
Assert.Equal(1, spring.Mass);
Assert.Equal(2, spring.Stiffness);
Assert.Equal(3, spring.Damping);
Assert.Equal(4, spring.InitialVelocity);
}
[Theory]
[InlineData("1,2F,3,4")]
[InlineData("Foo,Bar,Fee,Buzz")]
public void Can_Handle_Invalid_String_Via_TypeConverter(string input)
{
var conv = new SpringTypeConverter();
Assert.ThrowsAny<Exception>(() => (Spring)conv.ConvertFrom(input));
}
[Fact]
public void SplineEasing_Can_Be_Mutated()
{
var easing = new SpringEasing(1, 1, 1, 0);
Assert.Equal(0, easing.Ease(0));
Assert.Equal(0.34029984660829826, easing.Ease(1));
easing.Mass = 2;
easing.Stiffness = 2;
easing.Damping = 2;
easing.InitialVelocity = 1;
Assert.NotEqual(0.05136985716812037, easing.Ease(0.5));
}
[Fact]
public void Check_SpringEasing_Handled_properly()
{
var keyframe1 = new KeyFrame()
{
Setters =
{
new Setter(RotateTransform.AngleProperty, -2.5d),
},
KeyTime = TimeSpan.FromSeconds(0)
};
var keyframe2 = new KeyFrame()
{
Setters =
{
new Setter(RotateTransform.AngleProperty, 2.5d),
},
KeyTime = TimeSpan.FromSeconds(5)
};
var animation = new Avalonia.Animation.Animation()
{
Duration = TimeSpan.FromSeconds(5),
Children =
{
keyframe1,
keyframe2
},
IterationCount = new IterationCount(5),
PlaybackDirection = PlaybackDirection.Alternate,
Easing = new SpringEasing(1, 10, 1, 0)
};
var rotateTransform = new RotateTransform(-2.5);
var rect = new Rectangle()
{
RenderTransform = rotateTransform
};
var clock = new TestClock();
var animationRun = animation.RunAsync(rect, clock);
clock.Step(TimeSpan.Zero);
Assert.Equal(rotateTransform.Angle, -2.5);
clock.Step(TimeSpan.FromSeconds(5));
Assert.Equal(rotateTransform.Angle, 5.522828945000075);
var tolerance = 0.01;
clock.Step(TimeSpan.Parse("00:00:10.0153932"));
var expected = -2.499763294237805;
Assert.True(Math.Abs(rotateTransform.Angle - expected) <= tolerance);
clock.Step(TimeSpan.Parse("00:00:11.2655407"));
expected = -1.1011448950348934;
Assert.True(Math.Abs(rotateTransform.Angle - expected) <= tolerance);
clock.Step(TimeSpan.Parse("00:00:12.6158773"));
expected = 2.1264981706749007;
Assert.True(Math.Abs(rotateTransform.Angle - expected) <= tolerance);
clock.Step(TimeSpan.Parse("00:00:14.6495256"));
expected = 5.4337608446234782;
Assert.True(Math.Abs(rotateTransform.Angle - expected) <= tolerance);
}
}

58
tests/Avalonia.Base.UnitTests/Input/PointerOverTests.cs

@ -356,20 +356,28 @@ namespace Avalonia.Base.UnitTests.Input
var impl = CreateTopLevelImplMock(renderer.Object);
var invalidateRect = new Rect(0, 0, 15, 15);
var lastClientPosition = new Point(1, 5);
var result = new List<(object?, string, Point)>();
void HandleEvent(object? sender, PointerEventArgs e)
{
result.Add((sender, e.RoutedEvent!.Name, e.GetPosition(null)));
}
Canvas canvas;
var root = CreateInputRoot(impl.Object, new Panel
var root = (Window)CreateInputRoot(impl.Object, new Panel
{
Children =
{
(canvas = new Canvas())
}
});
AddEnteredExitedHandlers(HandleEvent, root, canvas);
// Let input know about latest device.
SetHit(renderer, canvas);
impl.Object.Input!(CreateRawPointerMovedArgs(deviceMock.Object, root));
impl.Object.Input!(CreateRawPointerMovedArgs(deviceMock.Object, root, lastClientPosition));
Assert.True(canvas.IsPointerOver);
SetHit(renderer, canvas);
@ -380,6 +388,52 @@ namespace Avalonia.Base.UnitTests.Input
SetHit(renderer, null);
renderer.Raise(r => r.SceneInvalidated += null, new SceneInvalidatedEventArgs((IRenderRoot)root, invalidateRect));
Assert.False(canvas.IsPointerOver);
Assert.Equal(
new[]
{
((object?)canvas, nameof(InputElement.PointerEntered), lastClientPosition),
(root, nameof(InputElement.PointerEntered), lastClientPosition),
(canvas, nameof(InputElement.PointerExited), lastClientPosition),
(root, nameof(InputElement.PointerExited), lastClientPosition),
},
result);
}
[Fact]
public void PointerOver_Invalidation_Should_Use_Previously_Captured_Element()
{
using var app = UnitTestApplication.Start(new TestServices(inputManager: new InputManager()));
var renderer = new Mock<IRenderer>();
var deviceMock = CreatePointerDeviceMock();
var impl = CreateTopLevelImplMock(renderer.Object);
var invalidateRect = new Rect(0, 0, 15, 15);
Canvas canvas1, canvas2;
var root = CreateInputRoot(impl.Object, new Panel
{
Children =
{
(canvas1 = new Canvas()),
(canvas2 = new Canvas())
}
});
canvas1.PointerMoved += (s, a) => a.Pointer.Capture(canvas1);
// Let input know about latest device.
SetHit(renderer, canvas1);
impl.Object.Input!(CreateRawPointerMovedArgs(deviceMock.Object, root));
Assert.True(canvas1.IsPointerOver);
Assert.False(canvas2.IsPointerOver);
SetHit(renderer, canvas2);
renderer.Raise(r => r.SceneInvalidated += null, new SceneInvalidatedEventArgs((IRenderRoot)root, invalidateRect));
Assert.False(canvas1.IsPointerOver);
Assert.True(canvas2.IsPointerOver);
}
// https://github.com/AvaloniaUI/Avalonia/issues/7748

2
tests/Avalonia.Base.UnitTests/Input/PointerTests.cs

@ -8,7 +8,7 @@ using Xunit;
namespace Avalonia.Base.UnitTests.Input
{
public class PointerTests
public class PointerTests : PointerTestsBase
{
[Fact]
public void On_Capture_Transfer_PointerCaptureLost_Should_Propagate_Up_To_The_Common_Parent()

2
tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextFormatterTests.cs

@ -389,7 +389,7 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting
if (textLine.Width > 300 || currentHeight + textLine.Height > 240)
{
textLine = textLine.Collapse(new TextTrailingWordEllipsis(new ReadOnlySlice<char>(new[] { TextTrimming.s_defaultEllipsisChar }), 300, defaultProperties));
textLine = textLine.Collapse(new TextTrailingWordEllipsis(new ReadOnlySlice<char>(new[] { TextTrimming.DefaultEllipsisChar }), 300, defaultProperties));
}
currentHeight += textLine.Height;

Loading…
Cancel
Save