Browse Source

Merge branch 'master' into feature/TextRunProperties

pull/4101/head
Benedikt Stebner 6 years ago
committed by GitHub
parent
commit
0c0bd004e3
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 5
      .ncrunch/NativeEmbedSample.v3.ncrunchproject
  2. 2
      nukebuild/Numerge
  3. 4
      packages/Avalonia/Avalonia.csproj
  4. 10
      packages/Avalonia/AvaloniaBuildTasks.props
  5. 22
      packages/Avalonia/AvaloniaBuildTasks.targets
  6. 18
      packages/Avalonia/AvaloniaItemSchema.xaml
  7. 2
      readme.md
  8. 6
      src/Avalonia.Base/Threading/DispatcherTimer.cs
  9. 4
      src/Avalonia.Build.Tasks/GenerateAvaloniaResourcesTask.cs
  10. 3
      src/Avalonia.Build.Tasks/XamlCompilerTaskExecutor.cs
  11. 67
      src/Avalonia.Controls/Repeater/ViewportManager.cs
  12. 2
      src/Avalonia.Controls/SelectionModel.cs
  13. 6
      src/Avalonia.Controls/TopLevel.cs
  14. 24
      src/Avalonia.Layout/EffectiveViewportChangedEventArgs.cs
  15. 12
      src/Avalonia.Layout/ILayoutManager.cs
  16. 7
      src/Avalonia.Layout/ILayoutable.cs
  17. 144
      src/Avalonia.Layout/LayoutManager.cs
  18. 70
      src/Avalonia.Layout/Layoutable.cs
  19. BIN
      src/Avalonia.Visuals/Assets/GraphemeBreak.trie
  20. BIN
      src/Avalonia.Visuals/Assets/UnicodeData.trie
  21. 2
      src/Avalonia.Visuals/Media/TextFormatting/Unicode/BiDiClass.cs
  22. 65
      src/Avalonia.Visuals/Media/TextFormatting/Unicode/BreakPairTable.cs
  23. 34
      src/Avalonia.Visuals/Media/TextFormatting/Unicode/GraphemeBreakClass.cs
  24. 4
      src/Avalonia.Visuals/Media/TextFormatting/Unicode/LineBreakClass.cs
  25. 4
      src/Avalonia.Visuals/Media/TextFormatting/Unicode/LineBreakEnumerator.cs
  26. 178
      src/Avalonia.Visuals/Media/TextFormatting/Unicode/PropertyValueAliasHelper.cs
  27. 10
      src/Avalonia.Visuals/Media/TextFormatting/Unicode/Script.cs
  28. 19
      src/Windows/Avalonia.Win32/WindowsMountedVolumeInfoListener.cs
  29. 424
      tests/Avalonia.Layout.UnitTests/LayoutableTests_EffectiveViewportChanged.cs
  30. 67
      tests/Avalonia.Visuals.UnitTests/Media/TextFormatting/BreakPairTable.txt
  31. 61
      tests/Avalonia.Visuals.UnitTests/Media/TextFormatting/GraphemeBreakClassTrieGenerator.cs
  32. 83
      tests/Avalonia.Visuals.UnitTests/Media/TextFormatting/GraphemeBreakClassTrieGeneratorTests.cs
  33. 2
      tests/Avalonia.Visuals.UnitTests/Media/TextFormatting/LineBreakerTests.cs
  34. 85
      tests/Avalonia.Visuals.UnitTests/Media/TextFormatting/TestDataGenerator.cs
  35. 61
      tests/Avalonia.Visuals.UnitTests/Media/TextFormatting/UnicodeDataGenerator.cs
  36. 25
      tests/Avalonia.Visuals.UnitTests/Media/TextFormatting/UnicodeDataGeneratorTests.cs
  37. 181
      tests/Avalonia.Visuals.UnitTests/Media/TextFormatting/UnicodeEnumsGenerator.cs

5
.ncrunch/NativeEmbedSample.v3.ncrunchproject

@ -0,0 +1,5 @@
<ProjectConfiguration>
<Settings>
<IgnoreThisComponentCompletely>True</IgnoreThisComponentCompletely>
</Settings>
</ProjectConfiguration>

2
nukebuild/Numerge

@ -1 +1 @@
Subproject commit 4464343aef5c8ab7a42fcb20a483a6058199f8b8 Subproject commit aef10ae67dc55c95f49b52a505a0be33bfa297a5

4
packages/Avalonia/Avalonia.csproj

@ -41,6 +41,10 @@
<Pack>true</Pack> <Pack>true</Pack>
<PackagePath>build\</PackagePath> <PackagePath>build\</PackagePath>
</Content> </Content>
<Content Include="AvaloniaItemSchema.xaml">
<Pack>true</Pack>
<PackagePath>build\</PackagePath>
</Content>
</ItemGroup> </ItemGroup>
<Import Project="..\..\build\SharedVersion.props" /> <Import Project="..\..\build\SharedVersion.props" />
<Import Project="..\..\build\NetFX.props" /> <Import Project="..\..\build\NetFX.props" />

10
packages/Avalonia/AvaloniaBuildTasks.props

@ -1,3 +1,11 @@
<Project DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003"> <Project DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<ItemGroup>
<AvailableItemName Include="AvaloniaXaml" />
<AvailableItemName Include="AvaloniaResource" />
<PropertyPageSchema Include="$(MSBuildThisFileDirectory)AvaloniaItemSchema.xaml" />
</ItemGroup>
<ItemGroup Condition="'$(EnableDefaultItems)'=='True'">
<AvaloniaXaml Include="**\*.axaml" SubType="Designer" />
<AvaloniaXaml Include="**\*.paml" SubType="Designer" />
</ItemGroup>
</Project> </Project>

22
packages/Avalonia/AvaloniaBuildTasks.targets

@ -4,6 +4,20 @@
<_AvaloniaUseExternalMSBuild Condition="'$(_AvaloniaForceInternalMSBuild)' == 'true'">false</_AvaloniaUseExternalMSBuild> <_AvaloniaUseExternalMSBuild Condition="'$(_AvaloniaForceInternalMSBuild)' == 'true'">false</_AvaloniaUseExternalMSBuild>
<AvaloniaXamlReportImportance Condition="'$(AvaloniaXamlReportImportance)' == ''">low</AvaloniaXamlReportImportance> <AvaloniaXamlReportImportance Condition="'$(AvaloniaXamlReportImportance)' == ''">low</AvaloniaXamlReportImportance>
</PropertyGroup> </PropertyGroup>
<!-- Unfortunately we have to update default items in .targets since custom nuget props are improted before Microsoft.NET.Sdk.DefaultItems.props -->
<ItemGroup Condition="'$(EnableDefaultItems)'=='True'">
<Compile Update="**\*.paml.cs">
<DependentUpon>%(Filename)</DependentUpon>
<SubType>Code</SubType>
</Compile>
<Compile Update="**\*.axaml.cs">
<DependentUpon>%(Filename)</DependentUpon>
<SubType>Code</SubType>
</Compile>
<None Remove="**\*.axaml" />
<None Remove="**\*.paml" />
</ItemGroup>
<UsingTask TaskName="GenerateAvaloniaResourcesTask" <UsingTask TaskName="GenerateAvaloniaResourcesTask"
AssemblyFile="$(AvaloniaBuildTasksLocation)" AssemblyFile="$(AvaloniaBuildTasksLocation)"
@ -31,9 +45,12 @@
<Target Name="GenerateAvaloniaResources" <Target Name="GenerateAvaloniaResources"
BeforeTargets="CoreCompile;CoreResGen" BeforeTargets="CoreCompile;CoreResGen"
Inputs="@(AvaloniaResource);$(MSBuildAllProjects)" Inputs="@(AvaloniaResource);@(AvaloniaXaml);$(MSBuildAllProjects)"
Outputs="$(AvaloniaResourcesTemporaryFilePath)" Outputs="$(AvaloniaResourcesTemporaryFilePath)"
DependsOnTargets="$(BuildAvaloniaResourcesDependsOn)"> DependsOnTargets="$(BuildAvaloniaResourcesDependsOn)">
<ItemGroup>
<AvaloniaResource Include="@(AvaloniaXaml)"/>
</ItemGroup>
<GenerateAvaloniaResourcesTask <GenerateAvaloniaResourcesTask
Condition="'$(_AvaloniaUseExternalMSBuild)' != 'true'" Condition="'$(_AvaloniaUseExternalMSBuild)' != 'true'"
Output="$(AvaloniaResourcesTemporaryFilePath)" Output="$(AvaloniaResourcesTemporaryFilePath)"
@ -79,5 +96,6 @@
<ItemGroup> <ItemGroup>
<UpToDateCheckInput Include="@(AvaloniaResource)" /> <UpToDateCheckInput Include="@(AvaloniaResource)" />
<UpToDateCheckInput Include="@(AvaloniaXaml)" />
</ItemGroup> </ItemGroup>
</Project> </Project>

18
packages/Avalonia/AvaloniaItemSchema.xaml

@ -0,0 +1,18 @@
<ProjectSchemaDefinitions xmlns="http://schemas.microsoft.com/build/2009/properties">
<ContentType
Name="AvaloniaXaml"
DisplayName="Avalonia XAML"
ItemType="AvaloniaXaml">
<NameValuePair Name="DependentFileExtensions" Value=".cs" />
<NameValuePair Name="DefaultMetadata_SubType" Value="Designer" />
</ContentType>
<ItemType Name="AvaloniaXaml" DisplayName="Avalonia XAML" />
<FileExtension Name=".axaml" ContentType="AvaloniaXaml" />
<FileExtension Name=".paml" ContentType="AvaloniaXaml" />
<ContentType
Name="AvaloniaResource"
DisplayName="Avalonia Resource"
ItemType="AvaloniaResource"
/>
</ProjectSchemaDefinitions>

2
readme.md

@ -68,7 +68,7 @@ Avalonia is licenced under the [MIT licence](licence.md).
## Contributors ## Contributors
This project exists thanks to all the people who contribute. [[Contribute](http://avaloniaui.net/contributing/contributing)]. This project exists thanks to all the people who contribute. [[Contribute](http://avaloniaui.net/contributing)].
<a href="https://github.com/AvaloniaUI/Avalonia/graphs/contributors"><img src="https://opencollective.com/Avalonia/contributors.svg?width=890&button=false" /></a> <a href="https://github.com/AvaloniaUI/Avalonia/graphs/contributors"><img src="https://opencollective.com/Avalonia/contributors.svg?width=890&button=false" /></a>
### Backers ### Backers

6
src/Avalonia.Base/Threading/DispatcherTimer.cs

@ -14,7 +14,7 @@ namespace Avalonia.Threading
private readonly DispatcherPriority _priority; private readonly DispatcherPriority _priority;
private TimeSpan _interval; private TimeSpan _interval;
/// <summary> /// <summary>
/// Initializes a new instance of the <see cref="DispatcherTimer"/> class. /// Initializes a new instance of the <see cref="DispatcherTimer"/> class.
/// </summary> /// </summary>
@ -154,6 +154,8 @@ namespace Avalonia.Threading
TimeSpan interval, TimeSpan interval,
DispatcherPriority priority = DispatcherPriority.Normal) DispatcherPriority priority = DispatcherPriority.Normal)
{ {
interval = (interval != TimeSpan.Zero) ? interval : TimeSpan.FromTicks(1);
var timer = new DispatcherTimer(priority) { Interval = interval }; var timer = new DispatcherTimer(priority) { Interval = interval };
timer.Tick += (s, e) => timer.Tick += (s, e) =>
@ -197,7 +199,7 @@ namespace Avalonia.Threading
} }
} }
/// <summary> /// <summary>
/// Raises the <see cref="Tick"/> event on the dispatcher thread. /// Raises the <see cref="Tick"/> event on the dispatcher thread.

4
src/Avalonia.Build.Tasks/GenerateAvaloniaResourcesTask.cs

@ -107,7 +107,7 @@ namespace Avalonia.Build.Tasks
foreach (var s in sources.ToList()) foreach (var s in sources.ToList())
{ {
if (s.Path.ToLowerInvariant().EndsWith(".xaml") || s.Path.ToLowerInvariant().EndsWith(".paml")) if (s.Path.ToLowerInvariant().EndsWith(".xaml") || s.Path.ToLowerInvariant().EndsWith(".paml") || s.Path.ToLowerInvariant().EndsWith(".axaml"))
{ {
XamlFileInfo info; XamlFileInfo info;
try try
@ -150,7 +150,7 @@ namespace Avalonia.Build.Tasks
BuildEngine.LogMessage($"GenerateAvaloniaResourcesTask -> Root: {Root}, {Resources?.Count()} resources, Output:{Output}", _reportImportance < MessageImportance.Low ? MessageImportance.High : _reportImportance); BuildEngine.LogMessage($"GenerateAvaloniaResourcesTask -> Root: {Root}, {Resources?.Count()} resources, Output:{Output}", _reportImportance < MessageImportance.Low ? MessageImportance.High : _reportImportance);
foreach (var r in EmbeddedResources.Where(r => r.ItemSpec.EndsWith(".xaml") || r.ItemSpec.EndsWith(".paml"))) foreach (var r in EmbeddedResources.Where(r => r.ItemSpec.EndsWith(".xaml") || r.ItemSpec.EndsWith(".paml") || r.ItemSpec.EndsWith(".axaml")))
BuildEngine.LogWarning(BuildEngineErrorCode.LegacyResmScheme, r.ItemSpec, BuildEngine.LogWarning(BuildEngineErrorCode.LegacyResmScheme, r.ItemSpec,
"XAML file is packed using legacy EmbeddedResource/resm scheme, relative URIs won't work"); "XAML file is packed using legacy EmbeddedResource/resm scheme, relative URIs won't work");
var resources = BuildResourceSources(); var resources = BuildResourceSources();

3
src/Avalonia.Build.Tasks/XamlCompilerTaskExecutor.cs

@ -25,7 +25,8 @@ namespace Avalonia.Build.Tasks
public static partial class XamlCompilerTaskExecutor public static partial class XamlCompilerTaskExecutor
{ {
static bool CheckXamlName(IResource r) => r.Name.ToLowerInvariant().EndsWith(".xaml") static bool CheckXamlName(IResource r) => r.Name.ToLowerInvariant().EndsWith(".xaml")
|| r.Name.ToLowerInvariant().EndsWith(".paml"); || r.Name.ToLowerInvariant().EndsWith(".paml")
|| r.Name.ToLowerInvariant().EndsWith(".axaml");
public class CompileResult public class CompileResult
{ {

67
src/Avalonia.Controls/Repeater/ViewportManager.cs

@ -49,8 +49,8 @@ namespace Avalonia.Controls
// For non-virtualizing layouts, we do not need to keep // For non-virtualizing layouts, we do not need to keep
// updating viewports and invalidating measure often. So when // updating viewports and invalidating measure often. So when
// a non virtualizing layout is used, we stop doing all that work. // a non virtualizing layout is used, we stop doing all that work.
bool _managingViewportDisabled; private bool _managingViewportDisabled;
private IDisposable _effectiveViewportChangedRevoker; private bool _effectiveViewportChangedSubscribed;
private bool _layoutUpdatedSubscribed; private bool _layoutUpdatedSubscribed;
public ViewportManager(ItemsRepeater owner) public ViewportManager(ItemsRepeater owner)
@ -228,11 +228,15 @@ namespace Avalonia.Controls
_pendingViewportShift = default; _pendingViewportShift = default;
_unshiftableShift = default; _unshiftableShift = default;
_effectiveViewportChangedRevoker?.Dispose(); if (_managingViewportDisabled && _effectiveViewportChangedSubscribed)
if (!_managingViewportDisabled)
{ {
_effectiveViewportChangedRevoker = SubscribeToEffectiveViewportChanged(_owner); _owner.EffectiveViewportChanged -= OnEffectiveViewportChanged;
_effectiveViewportChangedSubscribed = false;
}
else if (!_managingViewportDisabled && !_effectiveViewportChangedSubscribed)
{
_owner.EffectiveViewportChanged += OnEffectiveViewportChanged;
_effectiveViewportChangedSubscribed = true;
} }
} }
@ -340,6 +344,11 @@ namespace Avalonia.Controls
// Note that the element being brought into view could be a descendant. // Note that the element being brought into view could be a descendant.
var targetChild = GetImmediateChildOfRepeater((IControl)args.TargetObject); var targetChild = GetImmediateChildOfRepeater((IControl)args.TargetObject);
if (targetChild is null)
{
return;
}
// Make sure that only the target child can be the anchor during the bring into view operation. // Make sure that only the target child can be the anchor during the bring into view operation.
foreach (var child in _owner.Children) foreach (var child in _owner.Children)
{ {
@ -373,7 +382,7 @@ namespace Avalonia.Controls
if (parent == null) if (parent == null)
{ {
throw new InvalidOperationException("OnBringIntoViewRequested called with args.target element not under the ItemsRepeater that recieved the call"); return null;
} }
return targetChild; return targetChild;
@ -415,15 +424,15 @@ namespace Avalonia.Controls
_scroller = null; _scroller = null;
} }
_effectiveViewportChangedRevoker?.Dispose(); _owner.EffectiveViewportChanged -= OnEffectiveViewportChanged;
_effectiveViewportChangedRevoker = null; _effectiveViewportChangedSubscribed = false;
_ensuredScroller = false; _ensuredScroller = false;
} }
private void OnEffectiveViewportChanged(Rect effectiveViewport) private void OnEffectiveViewportChanged(object sender, EffectiveViewportChangedEventArgs e)
{ {
Logger.TryGet(LogEventLevel.Verbose, "Repeater")?.Log(this, "{LayoutId}: EffectiveViewportChanged event callback", _owner.Layout.LayoutId); Logger.TryGet(LogEventLevel.Verbose, "Repeater")?.Log(this, "{LayoutId}: EffectiveViewportChanged event callback", _owner.Layout.LayoutId);
UpdateViewport(effectiveViewport); UpdateViewport(e.EffectiveViewport);
_pendingViewportShift = default; _pendingViewportShift = default;
_unshiftableShift = default; _unshiftableShift = default;
@ -468,8 +477,8 @@ namespace Avalonia.Controls
} }
else if (!_managingViewportDisabled) else if (!_managingViewportDisabled)
{ {
_effectiveViewportChangedRevoker?.Dispose(); _owner.EffectiveViewportChanged += OnEffectiveViewportChanged;
_effectiveViewportChangedRevoker = SubscribeToEffectiveViewportChanged(_owner); _effectiveViewportChangedSubscribed = true;
} }
_ensuredScroller = true; _ensuredScroller = true;
@ -529,38 +538,6 @@ namespace Avalonia.Controls
} }
} }
private IDisposable SubscribeToEffectiveViewportChanged(IControl control)
{
// HACK: This is a bit of a hack. We need the effective viewport of the ItemsRepeater -
// we can get this from TransformedBounds, but this property is updated after layout has
// run, which is too late. Instead, for now lets just hook into an internal event on
// ScrollContentPresenter to find out what the offset and viewport will be after arrange
// and use those values. Note that this doesn't handle nested ScrollViewers at all, but
// it's enough to get scrolling to non-uniformly sized items working for now.
//
// UWP uses the EffectiveViewportChanged event (which I think was implemented specially
// for this case): we need to implement that in Avalonia, but the semantics of it aren't
// clear to me. Hopefully the source for this event will be released with WinUI 3.
if (control.VisualParent is ScrollContentPresenter scp)
{
scp.PreArrange += ScrollContentPresenterPreArrange;
return Disposable.Create(() => scp.PreArrange -= ScrollContentPresenterPreArrange);
}
return Disposable.Empty;
}
private void ScrollContentPresenterPreArrange(object sender, VectorEventArgs e)
{
var scp = (ScrollContentPresenter)sender;
var effectiveViewport = new Rect((Point)scp.Offset, new Size(e.Vector.X, e.Vector.Y));
if (effectiveViewport != _visibleWindow)
{
OnEffectiveViewportChanged(effectiveViewport);
}
}
private class ScrollerInfo private class ScrollerInfo
{ {
public ScrollerInfo(ScrollViewer scroller) public ScrollerInfo(ScrollViewer scroller)

2
src/Avalonia.Controls/SelectionModel.cs

@ -189,8 +189,6 @@ namespace Avalonia.Controls
} }
set set
{ {
var isSelected = IsSelectedWithPartialAt(value);
if (!IsSelectedAt(value) || SelectedItems.Count > 1) if (!IsSelectedAt(value) || SelectedItems.Count > 1)
{ {
using var operation = new Operation(this); using var operation = new Operation(this);

6
src/Avalonia.Controls/TopLevel.cs

@ -340,6 +340,9 @@ namespace Avalonia.Controls
_globalStyles.GlobalStylesRemoved -= ((IStyleHost)this).StylesRemoved; _globalStyles.GlobalStylesRemoved -= ((IStyleHost)this).StylesRemoved;
} }
Renderer?.Dispose();
Renderer = null;
var logicalArgs = new LogicalTreeAttachmentEventArgs(this, this, null); var logicalArgs = new LogicalTreeAttachmentEventArgs(this, this, null);
((ILogical)this).NotifyDetachedFromLogicalTree(logicalArgs); ((ILogical)this).NotifyDetachedFromLogicalTree(logicalArgs);
@ -349,8 +352,7 @@ namespace Avalonia.Controls
(this as IInputRoot).MouseDevice?.TopLevelClosed(this); (this as IInputRoot).MouseDevice?.TopLevelClosed(this);
PlatformImpl = null; PlatformImpl = null;
OnClosed(EventArgs.Empty); OnClosed(EventArgs.Empty);
Renderer?.Dispose();
Renderer = null;
LayoutManager?.Dispose(); LayoutManager?.Dispose();
} }

24
src/Avalonia.Layout/EffectiveViewportChangedEventArgs.cs

@ -0,0 +1,24 @@
using System;
namespace Avalonia.Layout
{
/// <summary>
/// Provides data for the <see cref="Layoutable.EffectiveViewportChanged"/> event.
/// </summary>
public class EffectiveViewportChangedEventArgs : EventArgs
{
public EffectiveViewportChangedEventArgs(Rect effectiveViewport)
{
EffectiveViewport = effectiveViewport;
}
/// <summary>
/// Gets the <see cref="Rect"/> representing the effective viewport.
/// </summary>
/// <remarks>
/// The viewport is expressed in coordinates relative to the control that the event is
/// raised on.
/// </remarks>
public Rect EffectiveViewport { get; }
}
}

12
src/Avalonia.Layout/ILayoutManager.cs

@ -54,5 +54,17 @@ namespace Avalonia.Layout
/// </remarks> /// </remarks>
[Obsolete("Call ExecuteInitialLayoutPass without parameter")] [Obsolete("Call ExecuteInitialLayoutPass without parameter")]
void ExecuteInitialLayoutPass(ILayoutRoot root); void ExecuteInitialLayoutPass(ILayoutRoot root);
/// <summary>
/// Registers a control as wanting to receive effective viewport notifications.
/// </summary>
/// <param name="control">The control.</param>
void RegisterEffectiveViewportListener(ILayoutable control);
/// <summary>
/// Registers a control as no longer wanting to receive effective viewport notifications.
/// </summary>
/// <param name="control">The control.</param>
void UnregisterEffectiveViewportListener(ILayoutable control);
} }
} }

7
src/Avalonia.Layout/ILayoutable.cs

@ -111,5 +111,12 @@ namespace Avalonia.Layout
/// </summary> /// </summary>
/// <param name="control">The child control.</param> /// <param name="control">The child control.</param>
void ChildDesiredSizeChanged(ILayoutable control); void ChildDesiredSizeChanged(ILayoutable control);
/// <summary>
/// Used by the <see cref="LayoutManager"/> to notify the control that its effective
/// viewport is changed.
/// </summary>
/// <param name="e">The viewport information.</param>
void EffectiveViewportChanged(EffectiveViewportChangedEventArgs e);
} }
} }

144
src/Avalonia.Layout/LayoutManager.cs

@ -1,7 +1,10 @@
using System; using System;
using System.Buffers;
using System.Collections.Generic;
using System.Diagnostics; using System.Diagnostics;
using Avalonia.Logging; using Avalonia.Logging;
using Avalonia.Threading; using Avalonia.Threading;
using Avalonia.VisualTree;
#nullable enable #nullable enable
@ -12,10 +15,12 @@ namespace Avalonia.Layout
/// </summary> /// </summary>
public class LayoutManager : ILayoutManager, IDisposable public class LayoutManager : ILayoutManager, IDisposable
{ {
private const int MaxPasses = 3;
private readonly ILayoutRoot _owner; private readonly ILayoutRoot _owner;
private readonly LayoutQueue<ILayoutable> _toMeasure = new LayoutQueue<ILayoutable>(v => !v.IsMeasureValid); private readonly LayoutQueue<ILayoutable> _toMeasure = new LayoutQueue<ILayoutable>(v => !v.IsMeasureValid);
private readonly LayoutQueue<ILayoutable> _toArrange = new LayoutQueue<ILayoutable>(v => !v.IsArrangeValid); private readonly LayoutQueue<ILayoutable> _toArrange = new LayoutQueue<ILayoutable>(v => !v.IsArrangeValid);
private readonly Action _executeLayoutPass; private readonly Action _executeLayoutPass;
private List<EffectiveViewportChangedListener>? _effectiveViewportChangedListeners;
private bool _disposed; private bool _disposed;
private bool _queued; private bool _queued;
private bool _running; private bool _running;
@ -92,8 +97,6 @@ namespace Avalonia.Layout
/// <inheritdoc/> /// <inheritdoc/>
public virtual void ExecuteLayoutPass() public virtual void ExecuteLayoutPass()
{ {
const int MaxPasses = 3;
Dispatcher.UIThread.VerifyAccess(); Dispatcher.UIThread.VerifyAccess();
if (_disposed) if (_disposed)
@ -103,8 +106,6 @@ namespace Avalonia.Layout
if (!_running) if (!_running)
{ {
_running = true;
Stopwatch? stopwatch = null; Stopwatch? stopwatch = null;
const LogEventLevel timingLogLevel = LogEventLevel.Information; const LogEventLevel timingLogLevel = LogEventLevel.Information;
@ -127,12 +128,13 @@ namespace Avalonia.Layout
try try
{ {
_running = true;
for (var pass = 0; pass < MaxPasses; ++pass) for (var pass = 0; pass < MaxPasses; ++pass)
{ {
ExecuteMeasurePass(); InnerLayoutPass();
ExecuteArrangePass();
if (_toMeasure.Count == 0) if (!RaiseEffectiveViewportChanged())
{ {
break; break;
} }
@ -202,6 +204,42 @@ namespace Avalonia.Layout
_toArrange.Dispose(); _toArrange.Dispose();
} }
void ILayoutManager.RegisterEffectiveViewportListener(ILayoutable control)
{
_effectiveViewportChangedListeners ??= new List<EffectiveViewportChangedListener>();
_effectiveViewportChangedListeners.Add(new EffectiveViewportChangedListener(
control,
CalculateEffectiveViewport(control)));
}
void ILayoutManager.UnregisterEffectiveViewportListener(ILayoutable control)
{
if (_effectiveViewportChangedListeners is object)
{
for (var i = _effectiveViewportChangedListeners.Count - 1; i >= 0; --i)
{
if (_effectiveViewportChangedListeners[i].Listener == control)
{
_effectiveViewportChangedListeners.RemoveAt(i);
}
}
}
}
private void InnerLayoutPass()
{
for (var pass = 0; pass < MaxPasses; ++pass)
{
ExecuteMeasurePass();
ExecuteArrangePass();
if (_toMeasure.Count == 0)
{
break;
}
}
}
private void ExecuteMeasurePass() private void ExecuteMeasurePass()
{ {
while (_toMeasure.Count > 0) while (_toMeasure.Count > 0)
@ -285,5 +323,97 @@ namespace Avalonia.Layout
_queued = true; _queued = true;
} }
} }
private bool RaiseEffectiveViewportChanged()
{
var startCount = _toMeasure.Count + _toArrange.Count;
if (_effectiveViewportChangedListeners is object)
{
var count = _effectiveViewportChangedListeners.Count;
var pool = ArrayPool<EffectiveViewportChangedListener>.Shared;
var listeners = pool.Rent(count);
_effectiveViewportChangedListeners.CopyTo(listeners);
try
{
for (var i = 0; i < count; ++i)
{
var l = _effectiveViewportChangedListeners[i];
if (!l.Listener.IsAttachedToVisualTree)
{
continue;
}
var viewport = CalculateEffectiveViewport(l.Listener);
if (viewport != l.Viewport)
{
l.Listener.EffectiveViewportChanged(new EffectiveViewportChangedEventArgs(viewport));
_effectiveViewportChangedListeners[i] = new EffectiveViewportChangedListener(l.Listener, viewport);
}
}
}
finally
{
pool.Return(listeners, clearArray: true);
}
}
return startCount != _toMeasure.Count + _toArrange.Count;
}
private Rect CalculateEffectiveViewport(IVisual control)
{
var viewport = new Rect(0, 0, double.PositiveInfinity, double.PositiveInfinity);
CalculateEffectiveViewport(control, control, ref viewport);
return viewport;
}
private void CalculateEffectiveViewport(IVisual target, IVisual control, ref Rect viewport)
{
// Recurse until the top level control.
if (control.VisualParent is object)
{
CalculateEffectiveViewport(target, control.VisualParent, ref viewport);
}
else
{
viewport = new Rect(control.Bounds.Size);
}
// Apply the control clip bounds if it's not the target control. We don't apply it to
// the target control because it may itself be clipped to bounds and if so the viewport
// we calculate would be of no use.
if (control != target && control.ClipToBounds)
{
viewport = control.Bounds.Intersect(viewport);
}
// Translate the viewport into this control's coordinate space.
viewport = viewport.Translate(-control.Bounds.Position);
if (control != target && control.RenderTransform is object)
{
var origin = control.RenderTransformOrigin.ToPixels(control.Bounds.Size);
var offset = Matrix.CreateTranslation(origin);
var renderTransform = (-offset) * control.RenderTransform.Value.Invert() * (offset);
viewport = viewport.TransformToAABB(renderTransform);
}
}
private readonly struct EffectiveViewportChangedListener
{
public EffectiveViewportChangedListener(ILayoutable listener, Rect viewport)
{
Listener = listener;
Viewport = viewport;
}
public ILayoutable Listener { get; }
public Rect Viewport { get; }
}
} }
} }

70
src/Avalonia.Layout/Layoutable.cs

@ -132,6 +132,7 @@ namespace Avalonia.Layout
private bool _measuring; private bool _measuring;
private Size? _previousMeasure; private Size? _previousMeasure;
private Rect? _previousArrange; private Rect? _previousArrange;
private EventHandler<EffectiveViewportChangedEventArgs>? _effectiveViewportChanged;
private EventHandler? _layoutUpdated; private EventHandler? _layoutUpdated;
/// <summary> /// <summary>
@ -152,6 +153,32 @@ namespace Avalonia.Layout
VerticalAlignmentProperty); VerticalAlignmentProperty);
} }
/// <summary>
/// Occurs when the element's effective viewport changes.
/// </summary>
public event EventHandler<EffectiveViewportChangedEventArgs>? EffectiveViewportChanged
{
add
{
if (_effectiveViewportChanged is null && VisualRoot is ILayoutRoot r)
{
r.LayoutManager.RegisterEffectiveViewportListener(this);
}
_effectiveViewportChanged += value;
}
remove
{
_effectiveViewportChanged -= value;
if (_effectiveViewportChanged is null && VisualRoot is ILayoutRoot r)
{
r.LayoutManager.UnregisterEffectiveViewportListener(this);
}
}
}
/// <summary> /// <summary>
/// Occurs when a layout pass completes for the control. /// Occurs when a layout pass completes for the control.
/// </summary> /// </summary>
@ -384,13 +411,6 @@ namespace Avalonia.Layout
} }
} }
/// <summary>
/// Called by InvalidateMeasure
/// </summary>
protected virtual void OnMeasureInvalidated()
{
}
/// <summary> /// <summary>
/// Invalidates the measurement of the control and queues a new layout pass. /// Invalidates the measurement of the control and queues a new layout pass.
/// </summary> /// </summary>
@ -436,6 +456,11 @@ namespace Avalonia.Layout
} }
} }
void ILayoutable.EffectiveViewportChanged(EffectiveViewportChangedEventArgs e)
{
_effectiveViewportChanged?.Invoke(this, e);
}
/// <summary> /// <summary>
/// Marks a property as affecting the control's measurement. /// Marks a property as affecting the control's measurement.
/// </summary> /// </summary>
@ -717,9 +742,17 @@ namespace Avalonia.Layout
{ {
base.OnAttachedToVisualTreeCore(e); base.OnAttachedToVisualTreeCore(e);
if (_layoutUpdated is object && e.Root is ILayoutRoot r) if (e.Root is ILayoutRoot r)
{ {
r.LayoutManager.LayoutUpdated += LayoutManagedLayoutUpdated; if (_layoutUpdated is object)
{
r.LayoutManager.LayoutUpdated += LayoutManagedLayoutUpdated;
}
if (_effectiveViewportChanged is object)
{
r.LayoutManager.RegisterEffectiveViewportListener(this);
}
} }
} }
@ -727,12 +760,27 @@ namespace Avalonia.Layout
{ {
base.OnDetachedFromVisualTreeCore(e); base.OnDetachedFromVisualTreeCore(e);
if (_layoutUpdated is object && e.Root is ILayoutRoot r) if (e.Root is ILayoutRoot r)
{ {
r.LayoutManager.LayoutUpdated -= LayoutManagedLayoutUpdated; if (_layoutUpdated is object)
{
r.LayoutManager.LayoutUpdated -= LayoutManagedLayoutUpdated;
}
if (_effectiveViewportChanged is object)
{
r.LayoutManager.UnregisterEffectiveViewportListener(this);
}
} }
} }
/// <summary>
/// Called by InvalidateMeasure
/// </summary>
protected virtual void OnMeasureInvalidated()
{
}
/// <inheritdoc/> /// <inheritdoc/>
protected sealed override void OnVisualParentChanged(IVisual oldParent, IVisual newParent) protected sealed override void OnVisualParentChanged(IVisual oldParent, IVisual newParent)
{ {

BIN
src/Avalonia.Visuals/Assets/GraphemeBreak.trie

Binary file not shown.

BIN
src/Avalonia.Visuals/Assets/UnicodeData.trie

Binary file not shown.

2
src/Avalonia.Visuals/Media/TextFormatting/Unicode/BiDiClass.cs

@ -2,6 +2,7 @@ namespace Avalonia.Media.TextFormatting.Unicode
{ {
public enum BiDiClass public enum BiDiClass
{ {
LeftToRight, //L
ArabicLetter, //AL ArabicLetter, //AL
ArabicNumber, //AN ArabicNumber, //AN
ParagraphSeparator, //B ParagraphSeparator, //B
@ -11,7 +12,6 @@ namespace Avalonia.Media.TextFormatting.Unicode
EuropeanSeparator, //ES EuropeanSeparator, //ES
EuropeanTerminator, //ET EuropeanTerminator, //ET
FirstStrongIsolate, //FSI FirstStrongIsolate, //FSI
LeftToRight, //L
LeftToRightEmbedding, //LRE LeftToRightEmbedding, //LRE
LeftToRightIsolate, //LRI LeftToRightIsolate, //LRI
LeftToRightOverride, //LRO LeftToRightOverride, //LRO

65
src/Avalonia.Visuals/Media/TextFormatting/Unicode/BreakPairTable.cs

@ -4,38 +4,39 @@ namespace Avalonia.Media.TextFormatting.Unicode
{ {
private static readonly byte[][] s_breakPairTable = private static readonly byte[][] s_breakPairTable =
{ {
new byte[] {4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,3,4,4,4,4,4,4,4,4,4,4}, new byte[] {4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,3,4,4,4,4,4,4,4,4,4,4,4},
new byte[] {0,4,4,1,1,4,4,4,4,1,1,0,0,0,0,0,1,1,0,0,4,2,4,0,0,0,0,0,0,0,0,1}, new byte[] {0,4,4,1,1,4,4,4,4,1,1,0,0,0,0,4,1,1,0,0,4,2,4,0,0,0,0,0,0,0,0,1,0},
new byte[] {0,4,4,1,1,4,4,4,4,1,1,1,1,1,0,0,1,1,0,0,4,2,4,0,0,0,0,0,0,0,0,1}, new byte[] {0,4,4,1,1,4,4,4,4,1,1,1,1,1,0,4,1,1,0,0,4,2,4,0,0,0,0,0,0,0,0,1,0},
new byte[] {4,4,4,1,1,1,4,4,4,1,1,1,1,1,1,1,1,1,1,1,4,2,4,1,1,1,1,1,1,1,1,1}, new byte[] {4,4,4,1,1,1,4,4,4,1,1,1,1,1,1,1,1,1,1,1,4,2,4,1,1,1,1,1,1,1,1,1,1},
new byte[] {1,4,4,1,1,1,4,4,4,1,1,1,1,1,1,1,1,1,1,1,4,2,4,1,1,1,1,1,1,1,1,1}, new byte[] {1,4,4,1,1,1,4,4,4,1,1,1,1,1,1,1,1,1,1,1,4,2,4,1,1,1,1,1,1,1,1,1,1},
new byte[] {0,4,4,1,1,1,4,4,4,0,0,0,0,0,0,0,1,1,0,0,4,2,4,0,0,0,0,0,0,0,0,1}, new byte[] {0,4,4,1,1,1,4,4,4,0,0,0,0,0,0,0,1,1,0,0,4,2,4,0,0,0,0,0,0,0,0,1,0},
new byte[] {0,4,4,1,1,1,4,4,4,0,0,0,0,0,0,1,1,1,0,0,4,2,4,0,0,0,0,0,0,0,0,1}, new byte[] {0,4,4,1,1,1,4,4,4,0,0,0,0,0,0,1,1,1,0,0,4,2,4,0,0,0,0,0,0,0,0,1,0},
new byte[] {0,4,4,1,1,1,4,4,4,0,0,1,0,1,0,0,1,1,0,0,4,2,4,0,0,0,0,0,0,0,0,1}, new byte[] {0,4,4,1,1,1,4,4,4,0,0,1,0,1,0,0,1,1,0,0,4,2,4,0,0,0,0,0,0,0,0,1,0},
new byte[] {0,4,4,1,1,1,4,4,4,0,0,1,1,1,0,0,1,1,0,0,4,2,4,0,0,0,0,0,0,0,0,1}, new byte[] {0,4,4,1,1,1,4,4,4,0,0,1,1,1,0,0,1,1,0,0,4,2,4,0,0,0,0,0,0,0,0,1,0},
new byte[] {1,4,4,1,1,1,4,4,4,0,0,1,1,1,1,0,1,1,0,0,4,2,4,1,1,1,1,1,0,1,1,1}, new byte[] {1,4,4,1,1,1,4,4,4,0,0,1,1,1,1,0,1,1,0,0,4,2,4,1,1,1,1,1,0,1,1,1,0},
new byte[] {1,4,4,1,1,1,4,4,4,0,0,1,1,1,0,0,1,1,0,0,4,2,4,0,0,0,0,0,0,0,0,1}, new byte[] {1,4,4,1,1,1,4,4,4,0,0,1,1,1,0,0,1,1,0,0,4,2,4,0,0,0,0,0,0,0,0,1,0},
new byte[] {1,4,4,1,1,1,4,4,4,1,1,1,1,1,0,1,1,1,0,0,4,2,4,0,0,0,0,0,0,0,0,1}, new byte[] {1,4,4,1,1,1,4,4,4,1,1,1,1,1,0,1,1,1,0,0,4,2,4,0,0,0,0,0,0,0,0,1,0},
new byte[] {1,4,4,1,1,1,4,4,4,1,1,1,1,1,0,1,1,1,0,0,4,2,4,0,0,0,0,0,0,0,0,1}, new byte[] {1,4,4,1,1,1,4,4,4,1,1,1,1,1,0,1,1,1,0,0,4,2,4,0,0,0,0,0,0,0,0,1,0},
new byte[] {1,4,4,1,1,1,4,4,4,1,1,1,1,1,0,1,1,1,0,0,4,2,4,0,0,0,0,0,0,0,0,1}, new byte[] {1,4,4,1,1,1,4,4,4,1,1,1,1,1,0,1,1,1,0,0,4,2,4,0,0,0,0,0,0,0,0,1,0},
new byte[] {0,4,4,1,1,1,4,4,4,0,1,0,0,0,0,1,1,1,0,0,4,2,4,0,0,0,0,0,0,0,0,1}, new byte[] {0,4,4,1,1,1,4,4,4,0,1,0,0,0,0,1,1,1,0,0,4,2,4,0,0,0,0,0,0,0,0,1,0},
new byte[] {0,4,4,1,1,1,4,4,4,0,0,0,0,0,0,1,1,1,0,0,4,2,4,0,0,0,0,0,0,0,0,1}, new byte[] {0,4,4,1,1,1,4,4,4,0,0,0,0,0,0,1,1,1,0,0,4,2,4,0,0,0,0,0,0,0,0,1,0},
new byte[] {0,4,4,1,0,1,4,4,4,0,0,1,0,0,0,0,1,1,0,0,4,2,4,0,0,0,0,0,0,0,0,1}, new byte[] {0,4,4,1,0,1,4,4,4,0,0,1,0,0,0,0,1,1,0,0,4,2,4,0,0,0,0,0,0,0,0,1,0},
new byte[] {0,4,4,1,0,1,4,4,4,0,0,0,0,0,0,0,1,1,0,0,4,2,4,0,0,0,0,0,0,0,0,1}, new byte[] {0,4,4,1,0,1,4,4,4,0,0,0,0,0,0,0,1,1,0,0,4,2,4,0,0,0,0,0,0,0,0,1,0},
new byte[] {1,4,4,1,1,1,4,4,4,1,1,1,1,1,1,1,1,1,1,1,4,2,4,1,1,1,1,1,1,1,1,1}, new byte[] {1,4,4,1,1,1,4,4,4,1,1,1,1,1,1,1,1,1,1,1,4,2,4,1,1,1,1,1,1,1,1,1,0},
new byte[] {0,4,4,1,1,1,4,4,4,0,0,0,0,0,0,0,1,1,0,4,4,2,4,0,0,0,0,0,0,0,0,1}, new byte[] {0,4,4,1,1,1,4,4,4,0,0,0,0,0,0,0,1,1,0,4,4,2,4,0,0,0,0,0,0,0,0,1,0},
new byte[] {0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,4,0,0,0,0,0,0,0,0,0,0,0}, new byte[] {0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,4,0,0,0,0,0,0,0,0,0,0,0,0},
new byte[] {1,4,4,1,1,1,4,4,4,1,1,1,1,1,0,1,1,1,0,0,4,2,4,0,0,0,0,0,0,0,0,1}, new byte[] {1,4,4,1,1,1,4,4,4,1,1,1,1,1,0,1,1,1,0,0,4,2,4,0,0,0,0,0,0,0,0,1,0},
new byte[] {1,4,4,1,1,1,4,4,4,1,1,1,1,1,1,1,1,1,1,1,4,2,4,1,1,1,1,1,1,1,1,1}, new byte[] {1,4,4,1,1,1,4,4,4,1,1,1,1,1,1,1,1,1,1,1,4,2,4,1,1,1,1,1,1,1,1,1,1},
new byte[] {0,4,4,1,1,1,4,4,4,0,1,0,0,0,0,1,1,1,0,0,4,2,4,0,0,0,1,1,0,0,0,1}, new byte[] {0,4,4,1,1,1,4,4,4,0,1,0,0,0,0,1,1,1,0,0,4,2,4,0,0,0,1,1,0,0,0,1,0},
new byte[] {0,4,4,1,1,1,4,4,4,0,1,0,0,0,0,1,1,1,0,0,4,2,4,0,0,0,0,1,0,0,0,1}, new byte[] {0,4,4,1,1,1,4,4,4,0,1,0,0,0,0,1,1,1,0,0,4,2,4,0,0,0,0,1,0,0,0,1,0},
new byte[] {0,4,4,1,1,1,4,4,4,0,1,0,0,0,0,1,1,1,0,0,4,2,4,1,1,1,1,0,0,0,0,1}, new byte[] {0,4,4,1,1,1,4,4,4,0,1,0,0,0,0,1,1,1,0,0,4,2,4,1,1,1,1,0,0,0,0,1,0},
new byte[] {0,4,4,1,1,1,4,4,4,0,1,0,0,0,0,1,1,1,0,0,4,2,4,0,0,0,1,1,0,0,0,1}, new byte[] {0,4,4,1,1,1,4,4,4,0,1,0,0,0,0,1,1,1,0,0,4,2,4,0,0,0,1,1,0,0,0,1,0},
new byte[] {0,4,4,1,1,1,4,4,4,0,1,0,0,0,0,1,1,1,0,0,4,2,4,0,0,0,0,1,0,0,0,1}, new byte[] {0,4,4,1,1,1,4,4,4,0,1,0,0,0,0,1,1,1,0,0,4,2,4,0,0,0,0,1,0,0,0,1,0},
new byte[] {0,4,4,1,1,1,4,4,4,0,0,0,0,0,0,0,1,1,0,0,4,2,4,0,0,0,0,0,1,0,0,1}, new byte[] {0,4,4,1,1,1,4,4,4,0,0,0,0,0,0,0,1,1,0,0,4,2,4,0,0,0,0,0,1,0,0,1,0},
new byte[] {0,4,4,1,1,1,4,4,4,0,1,0,0,0,0,1,1,1,0,0,4,2,4,0,0,0,0,0,0,0,1,1}, new byte[] {0,4,4,1,1,1,4,4,4,0,1,0,0,0,0,1,1,1,0,0,4,2,4,0,0,0,0,0,0,0,1,1,0},
new byte[] {0,4,4,1,1,1,4,4,4,0,1,0,0,0,0,1,1,1,0,0,4,2,4,0,0,0,0,0,0,0,0,1}, new byte[] {0,4,4,1,1,1,4,4,4,0,1,0,0,0,0,1,1,1,0,0,4,2,4,0,0,0,0,0,0,0,0,1,0},
new byte[] {0,4,4,1,1,1,4,4,4,0,0,0,0,0,1,0,1,1,0,0,4,2,4,0,0,0,0,0,0,1,1,1}, new byte[] {1,4,4,1,1,1,4,4,4,1,1,1,1,1,0,1,1,1,0,0,4,2,4,0,0,0,0,0,0,0,0,1,0},
new byte[] {0,4,4,1,1,0,4,4,4,0,0,0,0,0,0,0,0,0,0,0,4,2,4,0,0,0,0,0,0,0,0,1,0},
}; };
public static PairBreakType Map(LineBreakClass first, LineBreakClass second) public static PairBreakType Map(LineBreakClass first, LineBreakClass second)

34
src/Avalonia.Visuals/Media/TextFormatting/Unicode/GraphemeBreakClass.cs

@ -2,24 +2,20 @@ namespace Avalonia.Media.TextFormatting.Unicode
{ {
public enum GraphemeBreakClass public enum GraphemeBreakClass
{ {
Control, //CN Other,
CR, //CR CR,
EBase, //EB LF,
EBaseGAZ, //EBG Control,
EModifier, //EM Extend,
Extend, //EX ZWJ,
GlueAfterZwj, //GAZ RegionalIndicator,
L, //L Prepend,
LF, //LF SpacingMark,
LV, //LV L,
LVT, //LVT V,
Prepend, //PP T,
RegionalIndicator, //RI LV,
SpacingMark, //SM LVT,
T, //T ExtendedPictographic,
V, //V
Other, //XX
ZWJ, //ZWJ
ExtendedPictographic
} }
} }

4
src/Avalonia.Visuals/Media/TextFormatting/Unicode/LineBreakClass.cs

@ -34,10 +34,11 @@ namespace Avalonia.Media.TextFormatting.Unicode
EBase, //EB EBase, //EB
EModifier, //EM EModifier, //EM
ZWJ, //ZWJ ZWJ, //ZWJ
ContingentBreak, //CB
Unknown, //XX
Ambiguous, //AI Ambiguous, //AI
MandatoryBreak, //BK MandatoryBreak, //BK
ContingentBreak, //CB
ConditionalJapaneseStarter, //CJ ConditionalJapaneseStarter, //CJ
CarriageReturn, //CR CarriageReturn, //CR
LineFeed, //LF LineFeed, //LF
@ -45,6 +46,5 @@ namespace Avalonia.Media.TextFormatting.Unicode
ComplexContext, //SA ComplexContext, //SA
Surrogate, //SG Surrogate, //SG
Space, //SP Space, //SP
Unknown, //XX
} }
} }

4
src/Avalonia.Visuals/Media/TextFormatting/Unicode/LineBreakEnumerator.cs

@ -95,7 +95,8 @@ namespace Avalonia.Media.TextFormatting.Unicode
if (_nextClass.Value == LineBreakClass.MandatoryBreak) if (_nextClass.Value == LineBreakClass.MandatoryBreak)
{ {
Current = new LineBreak(FindPriorNonWhitespace(_lastPos), _lastPos); _lastPos = _pos;
Current = new LineBreak(FindPriorNonWhitespace(_lastPos), _lastPos, true);
return true; return true;
} }
@ -108,6 +109,7 @@ namespace Avalonia.Media.TextFormatting.Unicode
{ {
case PairBreakType.DI: // Direct break case PairBreakType.DI: // Direct break
shouldBreak = true; shouldBreak = true;
_lastPos = _pos;
break; break;
case PairBreakType.IN: // possible indirect break case PairBreakType.IN: // possible indirect break

178
src/Avalonia.Visuals/Media/TextFormatting/Unicode/PropertyValueAliasHelper.cs

@ -0,0 +1,178 @@
using System.Collections.Generic;
namespace Avalonia.Media.TextFormatting.Unicode
{
internal static class PropertyValueAliasHelper
{
private static readonly Dictionary<Script, string> s_scriptToTag =
new Dictionary<Script, string>{
{ Script.Unknown, "Zzzz"},
{ Script.Common, "Zyyy"},
{ Script.Inherited, "Zinh"},
{ Script.Adlam, "Adlm"},
{ Script.CaucasianAlbanian, "Aghb"},
{ Script.Ahom, "Ahom"},
{ Script.Arabic, "Arab"},
{ Script.ImperialAramaic, "Armi"},
{ Script.Armenian, "Armn"},
{ Script.Avestan, "Avst"},
{ Script.Balinese, "Bali"},
{ Script.Bamum, "Bamu"},
{ Script.BassaVah, "Bass"},
{ Script.Batak, "Batk"},
{ Script.Bengali, "Beng"},
{ Script.Bhaiksuki, "Bhks"},
{ Script.Bopomofo, "Bopo"},
{ Script.Brahmi, "Brah"},
{ Script.Braille, "Brai"},
{ Script.Buginese, "Bugi"},
{ Script.Buhid, "Buhd"},
{ Script.Chakma, "Cakm"},
{ Script.CanadianAboriginal, "Cans"},
{ Script.Carian, "Cari"},
{ Script.Cham, "Cham"},
{ Script.Cherokee, "Cher"},
{ Script.Chorasmian, "Chrs"},
{ Script.Coptic, "Copt"},
{ Script.Cypriot, "Cprt"},
{ Script.Cyrillic, "Cyrl"},
{ Script.Devanagari, "Deva"},
{ Script.DivesAkuru, "Diak"},
{ Script.Dogra, "Dogr"},
{ Script.Deseret, "Dsrt"},
{ Script.Duployan, "Dupl"},
{ Script.EgyptianHieroglyphs, "Egyp"},
{ Script.Elbasan, "Elba"},
{ Script.Elymaic, "Elym"},
{ Script.Ethiopic, "Ethi"},
{ Script.Georgian, "Geor"},
{ Script.Glagolitic, "Glag"},
{ Script.GunjalaGondi, "Gong"},
{ Script.MasaramGondi, "Gonm"},
{ Script.Gothic, "Goth"},
{ Script.Grantha, "Gran"},
{ Script.Greek, "Grek"},
{ Script.Gujarati, "Gujr"},
{ Script.Gurmukhi, "Guru"},
{ Script.Hangul, "Hang"},
{ Script.Han, "Hani"},
{ Script.Hanunoo, "Hano"},
{ Script.Hatran, "Hatr"},
{ Script.Hebrew, "Hebr"},
{ Script.Hiragana, "Hira"},
{ Script.AnatolianHieroglyphs, "Hluw"},
{ Script.PahawhHmong, "Hmng"},
{ Script.NyiakengPuachueHmong, "Hmnp"},
{ Script.KatakanaOrHiragana, "Hrkt"},
{ Script.OldHungarian, "Hung"},
{ Script.OldItalic, "Ital"},
{ Script.Javanese, "Java"},
{ Script.KayahLi, "Kali"},
{ Script.Katakana, "Kana"},
{ Script.Kharoshthi, "Khar"},
{ Script.Khmer, "Khmr"},
{ Script.Khojki, "Khoj"},
{ Script.KhitanSmallScript, "Kits"},
{ Script.Kannada, "Knda"},
{ Script.Kaithi, "Kthi"},
{ Script.TaiTham, "Lana"},
{ Script.Lao, "Laoo"},
{ Script.Latin, "Latn"},
{ Script.Lepcha, "Lepc"},
{ Script.Limbu, "Limb"},
{ Script.LinearA, "Lina"},
{ Script.LinearB, "Linb"},
{ Script.Lisu, "Lisu"},
{ Script.Lycian, "Lyci"},
{ Script.Lydian, "Lydi"},
{ Script.Mahajani, "Mahj"},
{ Script.Makasar, "Maka"},
{ Script.Mandaic, "Mand"},
{ Script.Manichaean, "Mani"},
{ Script.Marchen, "Marc"},
{ Script.Medefaidrin, "Medf"},
{ Script.MendeKikakui, "Mend"},
{ Script.MeroiticCursive, "Merc"},
{ Script.MeroiticHieroglyphs, "Mero"},
{ Script.Malayalam, "Mlym"},
{ Script.Modi, "Modi"},
{ Script.Mongolian, "Mong"},
{ Script.Mro, "Mroo"},
{ Script.MeeteiMayek, "Mtei"},
{ Script.Multani, "Mult"},
{ Script.Myanmar, "Mymr"},
{ Script.Nandinagari, "Nand"},
{ Script.OldNorthArabian, "Narb"},
{ Script.Nabataean, "Nbat"},
{ Script.Newa, "Newa"},
{ Script.Nko, "Nkoo"},
{ Script.Nushu, "Nshu"},
{ Script.Ogham, "Ogam"},
{ Script.OlChiki, "Olck"},
{ Script.OldTurkic, "Orkh"},
{ Script.Oriya, "Orya"},
{ Script.Osage, "Osge"},
{ Script.Osmanya, "Osma"},
{ Script.Palmyrene, "Palm"},
{ Script.PauCinHau, "Pauc"},
{ Script.OldPermic, "Perm"},
{ Script.PhagsPa, "Phag"},
{ Script.InscriptionalPahlavi, "Phli"},
{ Script.PsalterPahlavi, "Phlp"},
{ Script.Phoenician, "Phnx"},
{ Script.Miao, "Plrd"},
{ Script.InscriptionalParthian, "Prti"},
{ Script.Rejang, "Rjng"},
{ Script.HanifiRohingya, "Rohg"},
{ Script.Runic, "Runr"},
{ Script.Samaritan, "Samr"},
{ Script.OldSouthArabian, "Sarb"},
{ Script.Saurashtra, "Saur"},
{ Script.SignWriting, "Sgnw"},
{ Script.Shavian, "Shaw"},
{ Script.Sharada, "Shrd"},
{ Script.Siddham, "Sidd"},
{ Script.Khudawadi, "Sind"},
{ Script.Sinhala, "Sinh"},
{ Script.Sogdian, "Sogd"},
{ Script.OldSogdian, "Sogo"},
{ Script.SoraSompeng, "Sora"},
{ Script.Soyombo, "Soyo"},
{ Script.Sundanese, "Sund"},
{ Script.SylotiNagri, "Sylo"},
{ Script.Syriac, "Syrc"},
{ Script.Tagbanwa, "Tagb"},
{ Script.Takri, "Takr"},
{ Script.TaiLe, "Tale"},
{ Script.NewTaiLue, "Talu"},
{ Script.Tamil, "Taml"},
{ Script.Tangut, "Tang"},
{ Script.TaiViet, "Tavt"},
{ Script.Telugu, "Telu"},
{ Script.Tifinagh, "Tfng"},
{ Script.Tagalog, "Tglg"},
{ Script.Thaana, "Thaa"},
{ Script.Thai, "Thai"},
{ Script.Tibetan, "Tibt"},
{ Script.Tirhuta, "Tirh"},
{ Script.Ugaritic, "Ugar"},
{ Script.Vai, "Vaii"},
{ Script.WarangCiti, "Wara"},
{ Script.Wancho, "Wcho"},
{ Script.OldPersian, "Xpeo"},
{ Script.Cuneiform, "Xsux"},
{ Script.Yezidi, "Yezi"},
{ Script.Yi, "Yiii"},
{ Script.ZanabazarSquare, "Zanb"},
};
public static string GetTag(Script script)
{
if(!s_scriptToTag.ContainsKey(script))
{
return "Zzzz";
}
return s_scriptToTag[script];
}
}
}

10
src/Avalonia.Visuals/Media/TextFormatting/Unicode/Script.cs

@ -2,6 +2,9 @@ namespace Avalonia.Media.TextFormatting.Unicode
{ {
public enum Script public enum Script
{ {
Unknown, //Zzzz
Common, //Zyyy
Inherited, //Zinh
Adlam, //Adlm Adlam, //Adlm
CaucasianAlbanian, //Aghb CaucasianAlbanian, //Aghb
Ahom, //Ahom Ahom, //Ahom
@ -25,10 +28,12 @@ namespace Avalonia.Media.TextFormatting.Unicode
Carian, //Cari Carian, //Cari
Cham, //Cham Cham, //Cham
Cherokee, //Cher Cherokee, //Cher
Chorasmian, //Chrs
Coptic, //Copt Coptic, //Copt
Cypriot, //Cprt Cypriot, //Cprt
Cyrillic, //Cyrl Cyrillic, //Cyrl
Devanagari, //Deva Devanagari, //Deva
DivesAkuru, //Diak
Dogra, //Dogr Dogra, //Dogr
Deseret, //Dsrt Deseret, //Dsrt
Duployan, //Dupl Duployan, //Dupl
@ -63,6 +68,7 @@ namespace Avalonia.Media.TextFormatting.Unicode
Kharoshthi, //Khar Kharoshthi, //Khar
Khmer, //Khmr Khmer, //Khmr
Khojki, //Khoj Khojki, //Khoj
KhitanSmallScript, //Kits
Kannada, //Knda Kannada, //Knda
Kaithi, //Kthi Kaithi, //Kthi
TaiTham, //Lana TaiTham, //Lana
@ -151,10 +157,8 @@ namespace Avalonia.Media.TextFormatting.Unicode
Wancho, //Wcho Wancho, //Wcho
OldPersian, //Xpeo OldPersian, //Xpeo
Cuneiform, //Xsux Cuneiform, //Xsux
Yezidi, //Yezi
Yi, //Yiii Yi, //Yiii
ZanabazarSquare, //Zanb ZanabazarSquare, //Zanb
Inherited, //Zinh
Common, //Zyyy
Unknown, //Zzzz
} }
} }

19
src/Windows/Avalonia.Win32/WindowsMountedVolumeInfoListener.cs

@ -5,12 +5,13 @@ using System.Linq;
using System.Reactive.Disposables; using System.Reactive.Disposables;
using System.Reactive.Linq; using System.Reactive.Linq;
using Avalonia.Controls.Platform; using Avalonia.Controls.Platform;
using Avalonia.Logging;
namespace Avalonia.Win32 namespace Avalonia.Win32
{ {
internal class WindowsMountedVolumeInfoListener : IDisposable internal class WindowsMountedVolumeInfoListener : IDisposable
{ {
private readonly CompositeDisposable _disposables; private readonly CompositeDisposable _disposables;
private bool _beenDisposed = false; private bool _beenDisposed = false;
private ObservableCollection<MountedVolumeInfo> mountedDrives; private ObservableCollection<MountedVolumeInfo> mountedDrives;
@ -32,10 +33,22 @@ namespace Avalonia.Win32
var allDrives = DriveInfo.GetDrives(); var allDrives = DriveInfo.GetDrives();
var mountVolInfos = allDrives var mountVolInfos = allDrives
.Where(p => p.IsReady) .Where(p =>
{
try
{
var ret = p.IsReady;
return ret;
}
catch (Exception e)
{
Logger.TryGet(LogEventLevel.Warning, LogArea.Control)?.Log(this, $"Error in Windows drive enumeration: {e.Message}");
}
return false;
})
.Select(p => new MountedVolumeInfo() .Select(p => new MountedVolumeInfo()
{ {
VolumeLabel = string.IsNullOrEmpty(p.VolumeLabel.Trim()) ? p.RootDirectory.FullName VolumeLabel = string.IsNullOrEmpty(p.VolumeLabel.Trim()) ? p.RootDirectory.FullName
: $"{p.VolumeLabel} ({p.Name})", : $"{p.VolumeLabel} ({p.Name})",
VolumePath = p.RootDirectory.FullName, VolumePath = p.RootDirectory.FullName,
VolumeSizeBytes = (ulong)p.TotalSize VolumeSizeBytes = (ulong)p.TotalSize

424
tests/Avalonia.Layout.UnitTests/LayoutableTests_EffectiveViewportChanged.cs

@ -0,0 +1,424 @@
using System;
using System.Threading.Tasks;
using Avalonia.Controls;
using Avalonia.Controls.Presenters;
using Avalonia.Controls.Primitives;
using Avalonia.Controls.Templates;
using Avalonia.Media;
using Avalonia.UnitTests;
using Xunit;
namespace Avalonia.Layout.UnitTests
{
public class LayoutableTests_EffectiveViewportChanged
{
[Fact]
public async Task EffectiveViewportChanged_Not_Raised_When_Control_Added_To_Tree()
{
await RunOnUIThread.Execute(async () =>
{
var root = CreateRoot();
var target = new Canvas();
var raised = 0;
target.EffectiveViewportChanged += (s, e) =>
{
++raised;
};
root.Child = target;
Assert.Equal(0, raised);
});
}
[Fact]
public async Task EffectiveViewportChanged_Raised_Before_LayoutUpdated()
{
await RunOnUIThread.Execute(async () =>
{
var root = CreateRoot();
var target = new Canvas();
var raised = 0;
target.EffectiveViewportChanged += (s, e) =>
{
++raised;
};
root.Child = target;
await ExecuteInitialLayoutPass(root);
Assert.Equal(1, raised);
});
}
[Fact]
public async Task Parent_Affects_EffectiveViewport()
{
await RunOnUIThread.Execute(async () =>
{
var root = CreateRoot();
var target = new Canvas { Width = 100, Height = 100 };
var parent = new Border { Width = 200, Height = 200, Child = target };
var raised = 0;
root.Child = parent;
target.EffectiveViewportChanged += (s, e) =>
{
Assert.Equal(new Rect(-550, -400, 1200, 900), e.EffectiveViewport);
++raised;
};
await ExecuteInitialLayoutPass(root);
});
}
[Fact]
public async Task Invalidating_In_Handler_Causes_Layout_To_Be_Rerun_Before_LayoutUpdated_Raised()
{
await RunOnUIThread.Execute(async () =>
{
var root = CreateRoot();
var target = new TestCanvas();
var raised = 0;
var layoutUpdatedRaised = 0;
root.LayoutUpdated += (s, e) =>
{
Assert.Equal(2, target.MeasureCount);
Assert.Equal(2, target.ArrangeCount);
++layoutUpdatedRaised;
};
target.EffectiveViewportChanged += (s, e) =>
{
target.InvalidateMeasure();
++raised;
};
root.Child = target;
await ExecuteInitialLayoutPass(root);
Assert.Equal(1, raised);
Assert.Equal(1, layoutUpdatedRaised);
});
}
[Fact]
public async Task Viewport_Extends_Beyond_Centered_Control()
{
await RunOnUIThread.Execute(async () =>
{
var root = CreateRoot();
var target = new Canvas { Width = 52, Height = 52, };
var raised = 0;
target.EffectiveViewportChanged += (s, e) =>
{
Assert.Equal(new Rect(-574, -424, 1200, 900), e.EffectiveViewport);
++raised;
};
root.Child = target;
await ExecuteInitialLayoutPass(root);
Assert.Equal(1, raised);
});
}
[Fact]
public async Task Viewport_Extends_Beyond_Nested_Centered_Control()
{
await RunOnUIThread.Execute(async () =>
{
var root = CreateRoot();
var target = new Canvas { Width = 52, Height = 52 };
var parent = new Border { Width = 100, Height = 100, Child = target };
var raised = 0;
target.EffectiveViewportChanged += (s, e) =>
{
Assert.Equal(new Rect(-574, -424, 1200, 900), e.EffectiveViewport);
++raised;
};
root.Child = parent;
await ExecuteInitialLayoutPass(root);
Assert.Equal(1, raised);
});
}
[Fact]
public async Task ScrollViewer_Determines_EffectiveViewport()
{
await RunOnUIThread.Execute(async () =>
{
var root = CreateRoot();
var target = new Canvas { Width = 200, Height = 200 };
var scroller = new ScrollViewer { Width = 100, Height = 100, Content = target, Template = ScrollViewerTemplate() };
var raised = 0;
target.EffectiveViewportChanged += (s, e) =>
{
Assert.Equal(new Rect(0, 0, 100, 100), e.EffectiveViewport);
++raised;
};
root.Child = scroller;
await ExecuteInitialLayoutPass(root);
Assert.Equal(1, raised);
});
}
[Fact]
public async Task Scrolled_ScrollViewer_Determines_EffectiveViewport()
{
await RunOnUIThread.Execute(async () =>
{
var root = CreateRoot();
var target = new Canvas { Width = 200, Height = 200 };
var scroller = new ScrollViewer { Width = 100, Height = 100, Content = target, Template = ScrollViewerTemplate() };
var raised = 0;
root.Child = scroller;
await ExecuteInitialLayoutPass(root);
scroller.Offset = new Vector(0, 10);
await ExecuteScrollerLayoutPass(root, scroller, target, (s, e) =>
{
Assert.Equal(new Rect(0, 10, 100, 100), e.EffectiveViewport);
++raised;
});
Assert.Equal(1, raised);
});
}
[Fact]
public async Task Moving_Parent_Updates_EffectiveViewport()
{
await RunOnUIThread.Execute(async () =>
{
var root = CreateRoot();
var target = new Canvas { Width = 100, Height = 100 };
var parent = new Border { Width = 200, Height = 200, Child = target };
var raised = 0;
root.Child = parent;
await ExecuteInitialLayoutPass(root);
target.EffectiveViewportChanged += (s, e) =>
{
Assert.Equal(new Rect(-554, -400, 1200, 900), e.EffectiveViewport);
++raised;
};
parent.Margin = new Thickness(8, 0, 0, 0);
await ExecuteLayoutPass(root);
Assert.Equal(1, raised);
});
}
[Fact]
public async Task Translate_Transform_Doesnt_Affect_EffectiveViewport()
{
await RunOnUIThread.Execute(async () =>
{
var root = CreateRoot();
var target = new Canvas { Width = 100, Height = 100 };
var parent = new Border { Width = 200, Height = 200, Child = target };
var raised = 0;
root.Child = parent;
await ExecuteInitialLayoutPass(root);
target.EffectiveViewportChanged += (s, e) => ++raised;
target.RenderTransform = new TranslateTransform { X = 8 };
target.InvalidateMeasure();
await ExecuteLayoutPass(root);
Assert.Equal(0, raised);
});
}
[Fact]
public async Task Translate_Transform_On_Parent_Affects_EffectiveViewport()
{
await RunOnUIThread.Execute(async () =>
{
var root = CreateRoot();
var target = new Canvas { Width = 100, Height = 100 };
var parent = new Border { Width = 200, Height = 200, Child = target };
var raised = 0;
root.Child = parent;
await ExecuteInitialLayoutPass(root);
target.EffectiveViewportChanged += (s, e) =>
{
Assert.Equal(new Rect(-558, -400, 1200, 900), e.EffectiveViewport);
++raised;
};
// Change the parent render transform to move it. A layout is then needed before
// EffectiveViewportChanged is raised.
parent.RenderTransform = new TranslateTransform { X = 8 };
parent.InvalidateMeasure();
await ExecuteLayoutPass(root);
Assert.Equal(1, raised);
});
}
[Fact]
public async Task Rotate_Transform_On_Parent_Affects_EffectiveViewport()
{
await RunOnUIThread.Execute(async () =>
{
var root = CreateRoot();
var target = new Canvas { Width = 100, Height = 100 };
var parent = new Border { Width = 200, Height = 200, Child = target };
var raised = 0;
root.Child = parent;
await ExecuteInitialLayoutPass(root);
target.EffectiveViewportChanged += (s, e) =>
{
AssertArePixelEqual(new Rect(-651, -792, 1484, 1484), e.EffectiveViewport);
++raised;
};
parent.RenderTransformOrigin = new RelativePoint(0, 0, RelativeUnit.Absolute);
parent.RenderTransform = new RotateTransform { Angle = 45 };
parent.InvalidateMeasure();
await ExecuteLayoutPass(root);
Assert.Equal(1, raised);
});
}
private TestRoot CreateRoot() => new TestRoot { Width = 1200, Height = 900 };
private Task ExecuteInitialLayoutPass(TestRoot root)
{
root.LayoutManager.ExecuteInitialLayoutPass();
return Task.CompletedTask;
}
private Task ExecuteLayoutPass(TestRoot root)
{
root.LayoutManager.ExecuteLayoutPass();
return Task.CompletedTask;
}
private Task ExecuteScrollerLayoutPass(
TestRoot root,
ScrollViewer scroller,
Control target,
Action<object, EffectiveViewportChangedEventArgs> handler)
{
void ViewportChanged(object sender, EffectiveViewportChangedEventArgs e)
{
handler(sender, e);
}
target.EffectiveViewportChanged += ViewportChanged;
root.LayoutManager.ExecuteLayoutPass();
return Task.CompletedTask;
}
private IControlTemplate ScrollViewerTemplate()
{
return new FuncControlTemplate<ScrollViewer>((control, scope) => new Grid
{
ColumnDefinitions = new ColumnDefinitions
{
new ColumnDefinition(1, GridUnitType.Star),
new ColumnDefinition(GridLength.Auto),
},
RowDefinitions = new RowDefinitions
{
new RowDefinition(1, GridUnitType.Star),
new RowDefinition(GridLength.Auto),
},
Children =
{
new ScrollContentPresenter
{
Name = "PART_ContentPresenter",
[~ContentPresenter.ContentProperty] = control[~ContentControl.ContentProperty],
[~~ScrollContentPresenter.ExtentProperty] = control[~~ScrollViewer.ExtentProperty],
[~~ScrollContentPresenter.OffsetProperty] = control[~~ScrollViewer.OffsetProperty],
[~~ScrollContentPresenter.ViewportProperty] = control[~~ScrollViewer.ViewportProperty],
[~ScrollContentPresenter.CanHorizontallyScrollProperty] = control[~ScrollViewer.CanHorizontallyScrollProperty],
[~ScrollContentPresenter.CanVerticallyScrollProperty] = control[~ScrollViewer.CanVerticallyScrollProperty],
}.RegisterInNameScope(scope),
new ScrollBar
{
Name = "horizontalScrollBar",
Orientation = Orientation.Horizontal,
[~RangeBase.MaximumProperty] = control[~ScrollViewer.HorizontalScrollBarMaximumProperty],
[~~RangeBase.ValueProperty] = control[~~ScrollViewer.HorizontalScrollBarValueProperty],
[~ScrollBar.ViewportSizeProperty] = control[~ScrollViewer.HorizontalScrollBarViewportSizeProperty],
[~ScrollBar.VisibilityProperty] = control[~ScrollViewer.HorizontalScrollBarVisibilityProperty],
[Grid.RowProperty] = 1,
}.RegisterInNameScope(scope),
new ScrollBar
{
Name = "verticalScrollBar",
Orientation = Orientation.Vertical,
[~RangeBase.MaximumProperty] = control[~ScrollViewer.VerticalScrollBarMaximumProperty],
[~~RangeBase.ValueProperty] = control[~~ScrollViewer.VerticalScrollBarValueProperty],
[~ScrollBar.ViewportSizeProperty] = control[~ScrollViewer.VerticalScrollBarViewportSizeProperty],
[~ScrollBar.VisibilityProperty] = control[~ScrollViewer.VerticalScrollBarVisibilityProperty],
[Grid.ColumnProperty] = 1,
}.RegisterInNameScope(scope),
},
});
}
private void AssertArePixelEqual(Rect expected, Rect actual)
{
var expectedRounded = new Rect((int)expected.X, (int)expected.Y, (int)expected.Width, (int)expected.Height);
var actualRounded = new Rect((int)actual.X, (int)actual.Y, (int)actual.Width, (int)actual.Height);
Assert.Equal(expectedRounded, actualRounded);
}
private class TestCanvas : Canvas
{
public int MeasureCount { get; private set; }
public int ArrangeCount { get; private set; }
protected override Size MeasureOverride(Size availableSize)
{
++MeasureCount;
return base.MeasureOverride(availableSize);
}
protected override Size ArrangeOverride(Size finalSize)
{
++ArrangeCount;
return base.ArrangeOverride(finalSize);
}
}
private static class RunOnUIThread
{
public static async Task Execute(Func<Task> func)
{
await func();
}
}
}
}

67
tests/Avalonia.Visuals.UnitTests/Media/TextFormatting/BreakPairTable.txt

@ -1,33 +1,34 @@
OP CL CP QU GL NS EX SY IS PR PO NU AL HL ID IN HY BA BB B2 ZW CM WJ H2 H3 JL JV JT RI EB EM ZWJ OP CL CP QU GL NS EX SY IS PR PO NU AL HL ID IN HY BA BB B2 ZW CM WJ H2 H3 JL JV JT RI EB EM ZWJ CB
OP ^ ^ ^ ^ ^ ^ ^ ^ ^ ^ ^ ^ ^ ^ ^ ^ ^ ^ ^ ^ ^ @ ^ ^ ^ ^ ^ ^ ^ ^ ^ ^ OP ^ ^ ^ ^ ^ ^ ^ ^ ^ ^ ^ ^ ^ ^ ^ ^ ^ ^ ^ ^ ^ @ ^ ^ ^ ^ ^ ^ ^ ^ ^ ^ ^
CL _ ^ ^ % % ^ ^ ^ ^ % % _ _ _ _ _ % % _ _ ^ # ^ _ _ _ _ _ _ _ _ % CL _ ^ ^ % % ^ ^ ^ ^ % % _ _ _ _ ^ % % _ _ ^ # ^ _ _ _ _ _ _ _ _ % _
CP _ ^ ^ % % ^ ^ ^ ^ % % % % % _ _ % % _ _ ^ # ^ _ _ _ _ _ _ _ _ % CP _ ^ ^ % % ^ ^ ^ ^ % % % % % _ ^ % % _ _ ^ # ^ _ _ _ _ _ _ _ _ % _
QU ^ ^ ^ % % % ^ ^ ^ % % % % % % % % % % % ^ # ^ % % % % % % % % % QU ^ ^ ^ % % % ^ ^ ^ % % % % % % % % % % % ^ # ^ % % % % % % % % % %
GL % ^ ^ % % % ^ ^ ^ % % % % % % % % % % % ^ # ^ % % % % % % % % % GL % ^ ^ % % % ^ ^ ^ % % % % % % % % % % % ^ # ^ % % % % % % % % % %
NS _ ^ ^ % % % ^ ^ ^ _ _ _ _ _ _ _ % % _ _ ^ # ^ _ _ _ _ _ _ _ _ % NS _ ^ ^ % % % ^ ^ ^ _ _ _ _ _ _ _ % % _ _ ^ # ^ _ _ _ _ _ _ _ _ % _
EX _ ^ ^ % % % ^ ^ ^ _ _ _ _ _ _ % % % _ _ ^ # ^ _ _ _ _ _ _ _ _ % EX _ ^ ^ % % % ^ ^ ^ _ _ _ _ _ _ % % % _ _ ^ # ^ _ _ _ _ _ _ _ _ % _
SY _ ^ ^ % % % ^ ^ ^ _ _ % _ % _ _ % % _ _ ^ # ^ _ _ _ _ _ _ _ _ % SY _ ^ ^ % % % ^ ^ ^ _ _ % _ % _ _ % % _ _ ^ # ^ _ _ _ _ _ _ _ _ % _
IS _ ^ ^ % % % ^ ^ ^ _ _ % % % _ _ % % _ _ ^ # ^ _ _ _ _ _ _ _ _ % IS _ ^ ^ % % % ^ ^ ^ _ _ % % % _ _ % % _ _ ^ # ^ _ _ _ _ _ _ _ _ % _
PR % ^ ^ % % % ^ ^ ^ _ _ % % % % _ % % _ _ ^ # ^ % % % % % _ % % % PR % ^ ^ % % % ^ ^ ^ _ _ % % % % _ % % _ _ ^ # ^ % % % % % _ % % % _
PO % ^ ^ % % % ^ ^ ^ _ _ % % % _ _ % % _ _ ^ # ^ _ _ _ _ _ _ _ _ % PO % ^ ^ % % % ^ ^ ^ _ _ % % % _ _ % % _ _ ^ # ^ _ _ _ _ _ _ _ _ % _
NU % ^ ^ % % % ^ ^ ^ % % % % % _ % % % _ _ ^ # ^ _ _ _ _ _ _ _ _ % NU % ^ ^ % % % ^ ^ ^ % % % % % _ % % % _ _ ^ # ^ _ _ _ _ _ _ _ _ % _
AL % ^ ^ % % % ^ ^ ^ % % % % % _ % % % _ _ ^ # ^ _ _ _ _ _ _ _ _ % AL % ^ ^ % % % ^ ^ ^ % % % % % _ % % % _ _ ^ # ^ _ _ _ _ _ _ _ _ % _
HL % ^ ^ % % % ^ ^ ^ % % % % % _ % % % _ _ ^ # ^ _ _ _ _ _ _ _ _ % HL % ^ ^ % % % ^ ^ ^ % % % % % _ % % % _ _ ^ # ^ _ _ _ _ _ _ _ _ % _
ID _ ^ ^ % % % ^ ^ ^ _ % _ _ _ _ % % % _ _ ^ # ^ _ _ _ _ _ _ _ _ % ID _ ^ ^ % % % ^ ^ ^ _ % _ _ _ _ % % % _ _ ^ # ^ _ _ _ _ _ _ _ _ % _
IN _ ^ ^ % % % ^ ^ ^ _ _ _ _ _ _ % % % _ _ ^ # ^ _ _ _ _ _ _ _ _ % IN _ ^ ^ % % % ^ ^ ^ _ _ _ _ _ _ % % % _ _ ^ # ^ _ _ _ _ _ _ _ _ % _
HY _ ^ ^ % _ % ^ ^ ^ _ _ % _ _ _ _ % % _ _ ^ # ^ _ _ _ _ _ _ _ _ % HY _ ^ ^ % _ % ^ ^ ^ _ _ % _ _ _ _ % % _ _ ^ # ^ _ _ _ _ _ _ _ _ % _
BA _ ^ ^ % _ % ^ ^ ^ _ _ _ _ _ _ _ % % _ _ ^ # ^ _ _ _ _ _ _ _ _ % BA _ ^ ^ % _ % ^ ^ ^ _ _ _ _ _ _ _ % % _ _ ^ # ^ _ _ _ _ _ _ _ _ % _
BB % ^ ^ % % % ^ ^ ^ % % % % % % % % % % % ^ # ^ % % % % % % % % % BB % ^ ^ % % % ^ ^ ^ % % % % % % % % % % % ^ # ^ % % % % % % % % % _
B2 _ ^ ^ % % % ^ ^ ^ _ _ _ _ _ _ _ % % _ ^ ^ # ^ _ _ _ _ _ _ _ _ % B2 _ ^ ^ % % % ^ ^ ^ _ _ _ _ _ _ _ % % _ ^ ^ # ^ _ _ _ _ _ _ _ _ % _
ZW _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ ^ _ _ _ _ _ _ _ _ _ _ _ ZW _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ ^ _ _ _ _ _ _ _ _ _ _ _ _
CM % ^ ^ % % % ^ ^ ^ % % % % % _ % % % _ _ ^ # ^ _ _ _ _ _ _ _ _ % CM % ^ ^ % % % ^ ^ ^ % % % % % _ % % % _ _ ^ # ^ _ _ _ _ _ _ _ _ % _
WJ % ^ ^ % % % ^ ^ ^ % % % % % % % % % % % ^ # ^ % % % % % % % % % WJ % ^ ^ % % % ^ ^ ^ % % % % % % % % % % % ^ # ^ % % % % % % % % % %
H2 _ ^ ^ % % % ^ ^ ^ _ % _ _ _ _ % % % _ _ ^ # ^ _ _ _ % % _ _ _ % H2 _ ^ ^ % % % ^ ^ ^ _ % _ _ _ _ % % % _ _ ^ # ^ _ _ _ % % _ _ _ % _
H3 _ ^ ^ % % % ^ ^ ^ _ % _ _ _ _ % % % _ _ ^ # ^ _ _ _ _ % _ _ _ % H3 _ ^ ^ % % % ^ ^ ^ _ % _ _ _ _ % % % _ _ ^ # ^ _ _ _ _ % _ _ _ % _
JL _ ^ ^ % % % ^ ^ ^ _ % _ _ _ _ % % % _ _ ^ # ^ % % % % _ _ _ _ % JL _ ^ ^ % % % ^ ^ ^ _ % _ _ _ _ % % % _ _ ^ # ^ % % % % _ _ _ _ % _
JV _ ^ ^ % % % ^ ^ ^ _ % _ _ _ _ % % % _ _ ^ # ^ _ _ _ % % _ _ _ % JV _ ^ ^ % % % ^ ^ ^ _ % _ _ _ _ % % % _ _ ^ # ^ _ _ _ % % _ _ _ % _
JT _ ^ ^ % % % ^ ^ ^ _ % _ _ _ _ % % % _ _ ^ # ^ _ _ _ _ % _ _ _ % JT _ ^ ^ % % % ^ ^ ^ _ % _ _ _ _ % % % _ _ ^ # ^ _ _ _ _ % _ _ _ % _
RI _ ^ ^ % % % ^ ^ ^ _ _ _ _ _ _ _ % % _ _ ^ # ^ _ _ _ _ _ % _ _ % RI _ ^ ^ % % % ^ ^ ^ _ _ _ _ _ _ _ % % _ _ ^ # ^ _ _ _ _ _ % _ _ % _
EB _ ^ ^ % % % ^ ^ ^ _ % _ _ _ _ % % % _ _ ^ # ^ _ _ _ _ _ _ _ % % EB _ ^ ^ % % % ^ ^ ^ _ % _ _ _ _ % % % _ _ ^ # ^ _ _ _ _ _ _ _ % % _
EM _ ^ ^ % % % ^ ^ ^ _ % _ _ _ _ % % % _ _ ^ # ^ _ _ _ _ _ _ _ _ % EM _ ^ ^ % % % ^ ^ ^ _ % _ _ _ _ % % % _ _ ^ # ^ _ _ _ _ _ _ _ _ % _
ZWJ _ ^ ^ % % % ^ ^ ^ _ _ _ _ _ % _ % % _ _ ^ # ^ _ _ _ _ _ _ % % % ZWJ % ^ ^ % % % ^ ^ ^ % % % % % _ % % % _ _ ^ # ^ _ _ _ _ _ _ _ _ % _
CB _ ^ ^ % % _ ^ ^ ^ _ _ _ _ _ _ _ _ _ _ _ ^ # ^ _ _ _ _ _ _ _ _ % _

61
tests/Avalonia.Visuals.UnitTests/Media/TextFormatting/GraphemeBreakClassTrieGenerator.cs

@ -1,7 +1,6 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.IO; using System.IO;
using System.Linq;
using System.Net.Http; using System.Net.Http;
using System.Text.RegularExpressions; using System.Text.RegularExpressions;
using Avalonia.Media.TextFormatting.Unicode; using Avalonia.Media.TextFormatting.Unicode;
@ -12,6 +11,11 @@ namespace Avalonia.Visuals.UnitTests.Media.TextFormatting
{ {
public static void Execute() public static void Execute()
{ {
if (!Directory.Exists("Generated"))
{
Directory.CreateDirectory("Generated");
}
using (var stream = File.Create("Generated\\GraphemeBreak.trie")) using (var stream = File.Create("Generated\\GraphemeBreak.trie"))
{ {
var trie = GenerateBreakTypeTrie(); var trie = GenerateBreakTypeTrie();
@ -22,48 +26,29 @@ namespace Avalonia.Visuals.UnitTests.Media.TextFormatting
private static UnicodeTrie GenerateBreakTypeTrie() private static UnicodeTrie GenerateBreakTypeTrie()
{ {
var graphemeBreakClassValues = UnicodeEnumsGenerator.GetPropertyValueAliases("# Grapheme_Cluster_Break (GCB)");
var graphemeBreakClassMapping = graphemeBreakClassValues.Select(x => x.name).ToList();
var trieBuilder = new UnicodeTrieBuilder(); var trieBuilder = new UnicodeTrieBuilder();
var graphemeBreakData = ReadBreakData( var graphemeBreakData = ReadBreakData(Path.Combine(UnicodeDataGenerator.Ucd, "auxiliary/GraphemeBreakProperty.txt"));
"https://www.unicode.org/Public/UCD/latest/ucd/auxiliary/GraphemeBreakProperty.txt");
foreach (var (start, end, graphemeBreakType) in graphemeBreakData)
{
if (!graphemeBreakClassMapping.Contains(graphemeBreakType))
{
continue;
}
if (start == end)
{
trieBuilder.Set(start, (uint)graphemeBreakClassMapping.IndexOf(graphemeBreakType));
}
else
{
trieBuilder.SetRange(start, end, (uint)graphemeBreakClassMapping.IndexOf(graphemeBreakType));
}
}
var emojiBreakData = ReadBreakData("https://unicode.org/Public/emoji/12.0/emoji-data.txt"); var emojiBreakData = ReadBreakData(Path.Combine(UnicodeDataGenerator.Ucd, "emoji/emoji-data.txt"));
foreach (var (start, end, graphemeBreakType) in emojiBreakData) foreach (var breakData in new [] { graphemeBreakData, emojiBreakData })
{ {
if (!graphemeBreakClassMapping.Contains(graphemeBreakType)) foreach (var (start, end, graphemeBreakType) in breakData)
{ {
continue; if (!Enum.TryParse<GraphemeBreakClass>(graphemeBreakType, out var value))
} {
continue;
}
if (start == end) if (start == end)
{ {
trieBuilder.Set(start, (uint)graphemeBreakClassMapping.IndexOf(graphemeBreakType)); trieBuilder.Set(start, (uint)value);
} }
else else
{ {
trieBuilder.SetRange(start, end, (uint)graphemeBreakClassMapping.IndexOf(graphemeBreakType)); trieBuilder.SetRange(start, end, (uint)value);
}
} }
} }
@ -113,7 +98,9 @@ namespace Avalonia.Visuals.UnitTests.Media.TextFormatting
end = Convert.ToInt32(match.Groups[2].Value, 16); end = Convert.ToInt32(match.Groups[2].Value, 16);
} }
data.Add((start, end, match.Groups[3].Value)); var breakType = match.Groups[3].Value;
data.Add((start, end, breakType));
} }
} }
} }

83
tests/Avalonia.Visuals.UnitTests/Media/TextFormatting/GraphemeBreakClassTrieGeneratorTests.cs

@ -1,9 +1,4 @@
using System; using System;
using System.Collections;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Net.Http;
using Avalonia.Media.TextFormatting.Unicode; using Avalonia.Media.TextFormatting.Unicode;
using Xunit; using Xunit;
@ -16,10 +11,12 @@ namespace Avalonia.Visuals.UnitTests.Media.TextFormatting
public class GraphemeBreakClassTrieGeneratorTests public class GraphemeBreakClassTrieGeneratorTests
{ {
[Theory(Skip = "Only run when we update the trie.")] [Theory(Skip = "Only run when we update the trie.")]
[ClassData(typeof(GraphemeEnumeratorTestDataGenerator))] [ClassData(typeof(GraphemeBreakTestDataGenerator))]
public void Should_Enumerate(string text, int expectedLength) public void Should_Enumerate(string text, int expectedLength)
{ {
var enumerator = new GraphemeEnumerator(text.AsMemory()); var textMemory = text.AsMemory();
var enumerator = new GraphemeEnumerator(textMemory);
Assert.True(enumerator.MoveNext()); Assert.True(enumerator.MoveNext());
@ -31,7 +28,9 @@ namespace Avalonia.Visuals.UnitTests.Media.TextFormatting
{ {
const string text = "ABCDEFGHIJ"; const string text = "ABCDEFGHIJ";
var enumerator = new GraphemeEnumerator(text.AsMemory()); var textMemory = text.AsMemory();
var enumerator = new GraphemeEnumerator(textMemory);
var count = 0; var count = 0;
@ -51,73 +50,11 @@ namespace Avalonia.Visuals.UnitTests.Media.TextFormatting
GraphemeBreakClassTrieGenerator.Execute(); GraphemeBreakClassTrieGenerator.Execute();
} }
public class GraphemeEnumeratorTestDataGenerator : IEnumerable<object[]> private class GraphemeBreakTestDataGenerator : TestDataGenerator
{ {
private readonly List<object[]> _testData; public GraphemeBreakTestDataGenerator()
: base("auxiliary/GraphemeBreakTest.txt")
public GraphemeEnumeratorTestDataGenerator()
{
_testData = ReadTestData();
}
public IEnumerator<object[]> GetEnumerator()
{
return _testData.GetEnumerator();
}
IEnumerator IEnumerable.GetEnumerator()
{
return GetEnumerator();
}
private static List<object[]> ReadTestData()
{ {
var testData = new List<object[]>();
using (var client = new HttpClient())
{
using (var result = client.GetAsync("https://www.unicode.org/Public/UNIDATA/auxiliary/GraphemeBreakTest.txt").GetAwaiter().GetResult())
{
if (!result.IsSuccessStatusCode)
return testData;
using (var stream = result.Content.ReadAsStreamAsync().GetAwaiter().GetResult())
using (var reader = new StreamReader(stream))
{
while (!reader.EndOfStream)
{
var line = reader.ReadLine();
if (line == null)
{
break;
}
if (line.StartsWith("#") || string.IsNullOrEmpty(line))
{
continue;
}
var elements = line.Split('#')[0].Replace("÷\t", "÷").Trim('÷').Split('÷');
var chars = elements[0].Replace(" × ", " ").Split(' ');
var codepoints = chars.Where(x => x != "" && x != "×")
.Select(x => Convert.ToInt32(x, 16)).ToArray();
var text = string.Join(null, codepoints.Select(char.ConvertFromUtf32));
var length = codepoints.Select(x => x > ushort.MaxValue ? 2 : 1).Sum();
var data = new object[] { text, length };
testData.Add(data);
}
}
}
}
return testData;
} }
} }
} }

2
tests/Avalonia.Visuals.UnitTests/Media/TextFormatting/LineBreakerTests.cs

@ -3,7 +3,7 @@ using Avalonia.Media.TextFormatting.Unicode;
using Avalonia.Utilities; using Avalonia.Utilities;
using Xunit; using Xunit;
namespace Avalonia.Visuals.UnitTests.Media.Text namespace Avalonia.Visuals.UnitTests.Media.TextFormatting
{ {
public class LineBreakerTests public class LineBreakerTests
{ {

85
tests/Avalonia.Visuals.UnitTests/Media/TextFormatting/TestDataGenerator.cs

@ -0,0 +1,85 @@
using System;
using System.Collections;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Net.Http;
namespace Avalonia.Visuals.UnitTests.Media.TextFormatting
{
public abstract class TestDataGenerator : IEnumerable<object[]>
{
private readonly string _fileName;
private readonly List<object[]> _testData;
protected TestDataGenerator(string fileName)
{
_fileName = fileName;
_testData = ReadTestData();
}
public IEnumerator<object[]> GetEnumerator()
{
return _testData.GetEnumerator();
}
IEnumerator IEnumerable.GetEnumerator()
{
return GetEnumerator();
}
private List<object[]> ReadTestData()
{
var testData = new List<object[]>();
using (var client = new HttpClient())
{
var url = Path.Combine(UnicodeDataGenerator.Ucd, _fileName);
using (var result = client.GetAsync(url).GetAwaiter().GetResult())
{
if (!result.IsSuccessStatusCode)
return testData;
using (var stream = result.Content.ReadAsStreamAsync().GetAwaiter().GetResult())
using (var reader = new StreamReader(stream))
{
while (!reader.EndOfStream)
{
var line = reader.ReadLine();
if (line == null)
{
break;
}
if (line.StartsWith("#") || string.IsNullOrEmpty(line))
{
continue;
}
var elements = line.Split('#');
elements = elements[0].Replace("÷\t", "÷").Trim('÷').Split('÷');
var chars = elements[0].Replace(" × ", " ").Split(' ');
var codepoints = chars.Where(x => x != "" && x != "×")
.Select(x => Convert.ToInt32(x, 16)).ToArray();
var text = string.Join(null, codepoints.Select(char.ConvertFromUtf32));
var length = codepoints.Select(x => x > ushort.MaxValue ? 2 : 1).Sum();
var data = new object[] { text, length };
testData.Add(data);
}
}
}
}
return testData;
}
}
}

61
tests/Avalonia.Visuals.UnitTests/Media/TextFormatting/UnicodeDataGenerator.cs

@ -9,13 +9,16 @@ namespace Avalonia.Visuals.UnitTests.Media.TextFormatting
{ {
internal static class UnicodeDataGenerator internal static class UnicodeDataGenerator
{ {
public const string Ucd = "https://www.unicode.org/Public/13.0.0/ucd/";
public static void Execute() public static void Execute()
{ {
var codepoints = new Dictionary<int, UnicodeDataItem>(); var codepoints = new Dictionary<int, UnicodeDataItem>();
var generalCategoryValues = UnicodeEnumsGenerator.CreateGeneralCategoryEnum(); var generalCategoryEntries =
UnicodeEnumsGenerator.CreateGeneralCategoryEnum();
var generalCategoryMappings = CreateTagToIndexMappings(generalCategoryValues); var generalCategoryMappings = CreateTagToIndexMappings(generalCategoryEntries);
var generalCategoryData = ReadGeneralCategoryData(); var generalCategoryData = ReadGeneralCategoryData();
@ -26,23 +29,23 @@ namespace Avalonia.Visuals.UnitTests.Media.TextFormatting
AddGeneralCategoryRange(codepoints, range, generalCategory); AddGeneralCategoryRange(codepoints, range, generalCategory);
} }
var scriptValues = UnicodeEnumsGenerator.CreateScriptEnum(); var scriptEntries = UnicodeEnumsGenerator.CreateScriptEnum();
var scriptMappings = CreateNameToIndexMappings(scriptValues); var scriptMappings = CreateNameToIndexMappings(scriptEntries);
var scriptData = ReadScriptData(); var scriptData = ReadScriptData();
foreach (var (range, name) in scriptData) foreach (var (range, name) in scriptData)
{ {
var script = scriptMappings[name.Replace("_", "")]; var script = scriptMappings[name];
AddScriptRange(codepoints, range, script); AddScriptRange(codepoints, range, script);
} }
var biDiClassValues = UnicodeEnumsGenerator.CreateBiDiClassEnum(); var biDiClassEntries =
UnicodeEnumsGenerator.CreateBiDiClassEnum();
var biDiClassMappings = CreateTagToIndexMappings(biDiClassValues); var biDiClassMappings = CreateTagToIndexMappings(biDiClassEntries);
var biDiData = ReadBiDiData(); var biDiData = ReadBiDiData();
@ -53,9 +56,10 @@ namespace Avalonia.Visuals.UnitTests.Media.TextFormatting
AddBiDiClassRange(codepoints, range, biDiClass); AddBiDiClassRange(codepoints, range, biDiClass);
} }
var lineBreakClassValues = UnicodeEnumsGenerator.CreateLineBreakClassEnum(); var lineBreakClassEntries =
UnicodeEnumsGenerator.CreateLineBreakClassEnum();
var lineBreakClassMappings = CreateTagToIndexMappings(lineBreakClassValues); var lineBreakClassMappings = CreateTagToIndexMappings(lineBreakClassEntries);
var lineBreakClassData = ReadLineBreakClassData(); var lineBreakClassData = ReadLineBreakClassData();
@ -66,11 +70,11 @@ namespace Avalonia.Visuals.UnitTests.Media.TextFormatting
AddLineBreakClassRange(codepoints, range, lineBreakClass); AddLineBreakClassRange(codepoints, range, lineBreakClass);
} }
const int initialValue = ((int)LineBreakClass.Unknown << UnicodeData.LINEBREAK_SHIFT) | //const int initialValue = (0 << UnicodeData.LINEBREAK_SHIFT) |
((int)BiDiClass.LeftToRight << UnicodeData.BIDI_SHIFT) | // (0 << UnicodeData.BIDI_SHIFT) |
((int)Script.Unknown << UnicodeData.SCRIPT_SHIFT) | (int)GeneralCategory.Other; // (0 << UnicodeData.SCRIPT_SHIFT) | (int)GeneralCategory.Other;
var builder = new UnicodeTrieBuilder(initialValue); var builder = new UnicodeTrieBuilder(/*initialValue*/);
foreach (var properties in codepoints.Values) foreach (var properties in codepoints.Values)
{ {
@ -88,27 +92,30 @@ namespace Avalonia.Visuals.UnitTests.Media.TextFormatting
trie.Save(stream); trie.Save(stream);
} }
UnicodeEnumsGenerator.CreatePropertyValueAliasHelper(scriptEntries, generalCategoryEntries,
biDiClassEntries, lineBreakClassEntries);
} }
private static Dictionary<string, int> CreateTagToIndexMappings(List<(string name, string tag, string comment)> values) private static Dictionary<string, int> CreateTagToIndexMappings(List<DataEntry> entries)
{ {
var mappings = new Dictionary<string, int>(); var mappings = new Dictionary<string, int>();
for (var i = 0; i < values.Count; i++) for (var i = 0; i < entries.Count; i++)
{ {
mappings.Add(values[i].tag, i); mappings.Add(entries[i].Tag, i);
} }
return mappings; return mappings;
} }
private static Dictionary<string, int> CreateNameToIndexMappings(List<(string name, string tag, string comment)> values) private static Dictionary<string, int> CreateNameToIndexMappings(List<DataEntry> entries)
{ {
var mappings = new Dictionary<string, int>(); var mappings = new Dictionary<string, int>();
for (var i = 0; i < values.Count; i++) for (var i = 0; i < entries.Count; i++)
{ {
mappings.Add(values[i].name, i); mappings.Add(entries[i].Name, i);
} }
return mappings; return mappings;
@ -180,24 +187,22 @@ namespace Avalonia.Visuals.UnitTests.Media.TextFormatting
public static List<(CodepointRange, string)> ReadGeneralCategoryData() public static List<(CodepointRange, string)> ReadGeneralCategoryData()
{ {
return ReadUnicodeData( return ReadUnicodeData("extracted/DerivedGeneralCategory.txt");
"https://www.unicode.org/Public/UCD/latest/ucd/extracted/DerivedGeneralCategory.txt");
} }
public static List<(CodepointRange, string)> ReadScriptData() public static List<(CodepointRange, string)> ReadScriptData()
{ {
return ReadUnicodeData("https://www.unicode.org/Public/UCD/latest/ucd/Scripts.txt"); return ReadUnicodeData("Scripts.txt");
} }
public static List<(CodepointRange, string)> ReadBiDiData() public static List<(CodepointRange, string)> ReadBiDiData()
{ {
return ReadUnicodeData("https://www.unicode.org/Public/UCD/latest/ucd/extracted/DerivedBidiClass.txt"); return ReadUnicodeData("extracted/DerivedBidiClass.txt");
} }
public static List<(CodepointRange, string)> ReadLineBreakClassData() public static List<(CodepointRange, string)> ReadLineBreakClassData()
{ {
return ReadUnicodeData( return ReadUnicodeData("extracted/DerivedLineBreak.txt");
"https://www.unicode.org/Public/UCD/latest/ucd/extracted/DerivedLineBreak.txt");
} }
private static List<(CodepointRange, string)> ReadUnicodeData(string file) private static List<(CodepointRange, string)> ReadUnicodeData(string file)
@ -208,7 +213,9 @@ namespace Avalonia.Visuals.UnitTests.Media.TextFormatting
using (var client = new HttpClient()) using (var client = new HttpClient())
{ {
using (var result = client.GetAsync(file).GetAwaiter().GetResult()) var url = Path.Combine(Ucd, file);
using (var result = client.GetAsync(url).GetAwaiter().GetResult())
{ {
if (!result.IsSuccessStatusCode) if (!result.IsSuccessStatusCode)
{ {

25
tests/Avalonia.Visuals.UnitTests/Media/TextFormatting/UnicodeDataGeneratorTests.cs

@ -1,4 +1,6 @@
using Xunit; using System;
using Avalonia.Media.TextFormatting.Unicode;
using Xunit;
namespace Avalonia.Visuals.UnitTests.Media.TextFormatting namespace Avalonia.Visuals.UnitTests.Media.TextFormatting
{ {
@ -13,5 +15,26 @@ namespace Avalonia.Visuals.UnitTests.Media.TextFormatting
{ {
UnicodeDataGenerator.Execute(); UnicodeDataGenerator.Execute();
} }
[Theory(Skip = "Only run when we update the trie.")]
[ClassData(typeof(LineBreakTestDataGenerator))]
public void Should_Enumerate_LineBreaks(string text, int expectedLength)
{
var textMemory = text.AsMemory();
var enumerator = new LineBreakEnumerator(textMemory);
Assert.True(enumerator.MoveNext());
Assert.Equal(expectedLength, enumerator.Current.PositionWrap);
}
private class LineBreakTestDataGenerator : TestDataGenerator
{
public LineBreakTestDataGenerator()
: base("auxiliary/LineBreakTest.txt")
{
}
}
} }
} }

181
tests/Avalonia.Visuals.UnitTests/Media/TextFormatting/UnicodeEnumsGenerator.cs

@ -8,9 +8,16 @@ namespace Avalonia.Visuals.UnitTests.Media.TextFormatting
{ {
internal static class UnicodeEnumsGenerator internal static class UnicodeEnumsGenerator
{ {
public static List<(string name, string tag, string comment)> CreateScriptEnum() public static List<DataEntry> CreateScriptEnum()
{ {
var scriptValues = GetPropertyValueAliases("# Script (sc)"); var entries = new List<DataEntry>
{
new DataEntry("Unknown", "Zzzz", string.Empty),
new DataEntry("Common", "Zyyy", string.Empty),
new DataEntry("Inherited", "Zinh", string.Empty)
};
ParseDataEntries("# Script (sc)", entries);
using (var stream = File.Create("Generated\\Script.cs")) using (var stream = File.Create("Generated\\Script.cs"))
using (var writer = new StreamWriter(stream)) using (var writer = new StreamWriter(stream))
@ -20,22 +27,24 @@ namespace Avalonia.Visuals.UnitTests.Media.TextFormatting
writer.WriteLine(" public enum Script"); writer.WriteLine(" public enum Script");
writer.WriteLine(" {"); writer.WriteLine(" {");
foreach (var (name, tag, comment) in scriptValues) foreach (var entry in entries)
{ {
writer.WriteLine(" " + name + ", //" + tag + writer.WriteLine(" " + entry.Name.Replace("_", "") + ", //" + entry.Tag +
(string.IsNullOrEmpty(comment) ? string.Empty : "#" + comment)); (string.IsNullOrEmpty(entry.Comment) ? string.Empty : "#" + entry.Comment));
} }
writer.WriteLine(" }"); writer.WriteLine(" }");
writer.WriteLine("}"); writer.WriteLine("}");
} }
return scriptValues; return entries;
} }
public static List<(string name, string tag, string comment)> CreateGeneralCategoryEnum() public static List<DataEntry> CreateGeneralCategoryEnum()
{ {
var generalCategoryValues = GetPropertyValueAliases("# General_Category (gc)"); var entries = new List<DataEntry> { new DataEntry("Other", "C", " Cc | Cf | Cn | Co | Cs") };
ParseDataEntries("# General_Category (gc)", entries);
using (var stream = File.Create("Generated\\GeneralCategory.cs")) using (var stream = File.Create("Generated\\GeneralCategory.cs"))
using (var writer = new StreamWriter(stream)) using (var writer = new StreamWriter(stream))
@ -45,22 +54,24 @@ namespace Avalonia.Visuals.UnitTests.Media.TextFormatting
writer.WriteLine(" public enum GeneralCategory"); writer.WriteLine(" public enum GeneralCategory");
writer.WriteLine(" {"); writer.WriteLine(" {");
foreach (var (name, tag, comment) in generalCategoryValues) foreach (var entry in entries)
{ {
writer.WriteLine(" " + name + ", //" + tag + writer.WriteLine(" " + entry.Name.Replace("_", "") + ", //" + entry.Tag +
(string.IsNullOrEmpty(comment) ? string.Empty : "#" + comment)); (string.IsNullOrEmpty(entry.Comment) ? string.Empty : "#" + entry.Comment));
} }
writer.WriteLine(" }"); writer.WriteLine(" }");
writer.WriteLine("}"); writer.WriteLine("}");
} }
return generalCategoryValues; return entries;
} }
public static List<(string name, string tag, string comment)> CreateGraphemeBreakTypeEnum() public static List<DataEntry> CreateGraphemeBreakTypeEnum()
{ {
var graphemeClusterBreakValues = GetPropertyValueAliases("# Grapheme_Cluster_Break (GCB)"); var entries = new List<DataEntry> { new DataEntry("Other", "XX", string.Empty) };
ParseDataEntries("# Grapheme_Cluster_Break (GCB)", entries);
using (var stream = File.Create("Generated\\GraphemeBreakClass.cs")) using (var stream = File.Create("Generated\\GraphemeBreakClass.cs"))
using (var writer = new StreamWriter(stream)) using (var writer = new StreamWriter(stream))
@ -70,10 +81,10 @@ namespace Avalonia.Visuals.UnitTests.Media.TextFormatting
writer.WriteLine(" public enum GraphemeBreakClass"); writer.WriteLine(" public enum GraphemeBreakClass");
writer.WriteLine(" {"); writer.WriteLine(" {");
foreach (var (name, tag, comment) in graphemeClusterBreakValues) foreach (var entry in entries)
{ {
writer.WriteLine(" " + name + ", //" + tag + writer.WriteLine(" " + entry.Name.Replace("_", "") + ", //" + entry.Tag +
(string.IsNullOrEmpty(comment) ? string.Empty : "#" + comment)); (string.IsNullOrEmpty(entry.Comment) ? string.Empty : "#" + entry.Comment));
} }
writer.WriteLine(" ExtendedPictographic"); writer.WriteLine(" ExtendedPictographic");
@ -82,7 +93,7 @@ namespace Avalonia.Visuals.UnitTests.Media.TextFormatting
writer.WriteLine("}"); writer.WriteLine("}");
} }
return graphemeClusterBreakValues; return entries;
} }
private static List<string> GenerateBreakPairTable() private static List<string> GenerateBreakPairTable()
@ -185,20 +196,32 @@ namespace Avalonia.Visuals.UnitTests.Media.TextFormatting
} }
} }
public static List<(string name, string tag, string comment)> CreateLineBreakClassEnum() public static List<DataEntry> CreateLineBreakClassEnum()
{ {
var usedLineBreakClasses = GenerateBreakPairTable(); var usedLineBreakClasses = GenerateBreakPairTable();
var lineBreakValues = GetPropertyValueAliases("# Line_Break (lb)"); var entries = new List<DataEntry> { new DataEntry("Unknown", "XX", string.Empty) };
ParseDataEntries("# Line_Break (lb)", entries);
var lineBreakClassMappings = lineBreakValues.ToDictionary(x => x.tag, x => (x.name, x.tag, x.comment)); var orderedLineBreakEntries = new Dictionary<string, DataEntry>();
var orderedLineBreakValues = usedLineBreakClasses.Select(x => foreach (var tag in usedLineBreakClasses)
{ {
var value = lineBreakClassMappings[x]; var entry = entries.Single(x => x.Tag == tag);
lineBreakClassMappings.Remove(x);
return value; orderedLineBreakEntries.Add(tag, entry);
}).ToList(); }
foreach (var entry in entries)
{
if (orderedLineBreakEntries.ContainsKey(entry.Tag))
{
continue;
}
orderedLineBreakEntries.Add(entry.Tag, entry);
}
using (var stream = File.Create("Generated\\LineBreakClass.cs")) using (var stream = File.Create("Generated\\LineBreakClass.cs"))
using (var writer = new StreamWriter(stream)) using (var writer = new StreamWriter(stream))
@ -208,32 +231,24 @@ namespace Avalonia.Visuals.UnitTests.Media.TextFormatting
writer.WriteLine(" public enum LineBreakClass"); writer.WriteLine(" public enum LineBreakClass");
writer.WriteLine(" {"); writer.WriteLine(" {");
foreach (var (name, tag, comment) in orderedLineBreakValues) foreach (var entry in orderedLineBreakEntries.Values)
{
writer.WriteLine(" " + name + ", //" + tag +
(string.IsNullOrEmpty(comment) ? string.Empty : "#" + comment));
}
writer.WriteLine();
foreach (var (name, tag, comment) in lineBreakClassMappings.Values)
{ {
writer.WriteLine(" " + name + ", //" + tag + writer.WriteLine(" " + entry.Name.Replace("_", "") + ", //" + entry.Tag +
(string.IsNullOrEmpty(comment) ? string.Empty : "#" + comment)); (string.IsNullOrEmpty(entry.Comment) ? string.Empty : "#" + entry.Comment));
} }
writer.WriteLine(" }"); writer.WriteLine(" }");
writer.WriteLine("}"); writer.WriteLine("}");
} }
orderedLineBreakValues.AddRange(lineBreakClassMappings.Values); return orderedLineBreakEntries.Values.ToList();
return orderedLineBreakValues;
} }
public static List<(string name, string tag, string comment)> CreateBiDiClassEnum() public static List<DataEntry> CreateBiDiClassEnum()
{ {
var biDiClassValues = GetPropertyValueAliases("# Bidi_Class (bc)"); var entries = new List<DataEntry> { new DataEntry("Left_To_Right", "L", string.Empty) };
ParseDataEntries("# Bidi_Class (bc)", entries);
using (var stream = File.Create("Generated\\BiDiClass.cs")) using (var stream = File.Create("Generated\\BiDiClass.cs"))
using (var writer = new StreamWriter(stream)) using (var writer = new StreamWriter(stream))
@ -243,23 +258,21 @@ namespace Avalonia.Visuals.UnitTests.Media.TextFormatting
writer.WriteLine(" public enum BiDiClass"); writer.WriteLine(" public enum BiDiClass");
writer.WriteLine(" {"); writer.WriteLine(" {");
foreach (var (name, tag, comment) in biDiClassValues) foreach (var entry in entries)
{ {
writer.WriteLine(" " + name + ", //" + tag + writer.WriteLine(" " + entry.Name.Replace("_", "") + ", //" + entry.Tag +
(string.IsNullOrEmpty(comment) ? string.Empty : "#" + comment)); (string.IsNullOrEmpty(entry.Comment) ? string.Empty : "#" + entry.Comment));
} }
writer.WriteLine(" }"); writer.WriteLine(" }");
writer.WriteLine("}"); writer.WriteLine("}");
} }
return biDiClassValues; return entries;
} }
public static void CreatePropertyValueAliasHelper(List<(string name, string tag, string comment)> scriptValues, public static void CreatePropertyValueAliasHelper(List<DataEntry> scriptEntries, IEnumerable<DataEntry> generalCategoryEntries,
List<(string name, string tag, string comment)> generalCategoryValues, IEnumerable<DataEntry> biDiClassEntries, IEnumerable<DataEntry> lineBreakClassEntries)
List<(string name, string tag, string comment)> biDiClassValues,
List<(string name, string tag, string comment)> lineBreakValues)
{ {
using (var stream = File.Create("Generated\\PropertyValueAliasHelper.cs")) using (var stream = File.Create("Generated\\PropertyValueAliasHelper.cs"))
using (var writer = new StreamWriter(stream)) using (var writer = new StreamWriter(stream))
@ -269,35 +282,35 @@ namespace Avalonia.Visuals.UnitTests.Media.TextFormatting
writer.WriteLine("namespace Avalonia.Media.TextFormatting.Unicode"); writer.WriteLine("namespace Avalonia.Media.TextFormatting.Unicode");
writer.WriteLine("{"); writer.WriteLine("{");
writer.WriteLine(" public static class PropertyValueAliasHelper"); writer.WriteLine(" internal static class PropertyValueAliasHelper");
writer.WriteLine(" {"); writer.WriteLine(" {");
WritePropertyValueAliasGetTag(writer, scriptValues, "Script", "Zzzz"); WritePropertyValueAliasGetTag(writer, scriptEntries, "Script", "Zzzz");
WritePropertyValueAlias(writer, scriptValues, "Script", "Unknown"); WritePropertyValueAlias(writer, scriptEntries, "Script", "Unknown");
WritePropertyValueAlias(writer, generalCategoryValues, "GeneralCategory", "Other"); WritePropertyValueAlias(writer, generalCategoryEntries, "GeneralCategory", "Other");
WritePropertyValueAlias(writer, biDiClassValues, "BiDiClass", "LeftToRight"); WritePropertyValueAlias(writer, biDiClassEntries, "BiDiClass", "LeftToRight");
WritePropertyValueAlias(writer, lineBreakValues, "LineBreakClass", "Unknown"); WritePropertyValueAlias(writer, lineBreakClassEntries, "LineBreakClass", "Unknown");
writer.WriteLine(" }"); writer.WriteLine(" }");
writer.WriteLine("}"); writer.WriteLine("}");
} }
} }
public static List<(string name, string tag, string comment)> GetPropertyValueAliases(string property) public static void ParseDataEntries(string property, List<DataEntry> entries)
{ {
var data = new List<(string name, string tag, string comment)>();
using (var client = new HttpClient()) using (var client = new HttpClient())
{ {
using (var result = client.GetAsync("https://www.unicode.org/Public/UCD/latest/ucd/PropertyValueAliases.txt").GetAwaiter().GetResult()) var url = Path.Combine(UnicodeDataGenerator.Ucd, "PropertyValueAliases.txt");
using (var result = client.GetAsync(url).GetAwaiter().GetResult())
{ {
if (!result.IsSuccessStatusCode) if (!result.IsSuccessStatusCode)
{ {
return data; return;
} }
using (var stream = result.Content.ReadAsStreamAsync().GetAwaiter().GetResult()) using (var stream = result.Content.ReadAsStreamAsync().GetAwaiter().GetResult())
@ -337,7 +350,12 @@ namespace Avalonia.Visuals.UnitTests.Media.TextFormatting
elements = elements[2].Split('#'); elements = elements[2].Split('#');
var name = elements[0].Trim().Replace("_", string.Empty); var name = elements[0].Trim();
if (entries.Any(x => x.Name == name))
{
continue;
}
var comment = string.Empty; var comment = string.Empty;
@ -346,24 +364,25 @@ namespace Avalonia.Visuals.UnitTests.Media.TextFormatting
comment = elements[1]; comment = elements[1];
} }
data.Add((name, tag, comment)); var entry = new DataEntry(name, tag, comment);
entries.Add(entry);
} }
} }
} }
} }
return data;
} }
private static void WritePropertyValueAliasGetTag(TextWriter writer, private static void WritePropertyValueAliasGetTag(TextWriter writer, IEnumerable<DataEntry> entries,
IEnumerable<(string name, string tag, string comment)> values, string typeName, string defaultValue) string typeName, string defaultValue)
{ {
writer.WriteLine($" private static readonly Dictionary<{typeName}, string> s_{typeName.ToLower()}ToTag = "); writer.WriteLine(
$" private static readonly Dictionary<{typeName}, string> s_{typeName.ToLower()}ToTag = ");
writer.WriteLine($" new Dictionary<{typeName}, string>{{"); writer.WriteLine($" new Dictionary<{typeName}, string>{{");
foreach (var (name, tag, comment) in values) foreach (var entry in entries)
{ {
writer.WriteLine($" {{ {typeName}.{name}, \"{tag}\"}},"); writer.WriteLine($" {{ {typeName}.{entry.Name.Replace("_", "")}, \"{entry.Tag}\"}},");
} }
writer.WriteLine(" };"); writer.WriteLine(" };");
@ -382,15 +401,15 @@ namespace Avalonia.Visuals.UnitTests.Media.TextFormatting
writer.WriteLine(); writer.WriteLine();
} }
private static void WritePropertyValueAlias(TextWriter writer, private static void WritePropertyValueAlias(TextWriter writer, IEnumerable<DataEntry> entries, string typeName,
IEnumerable<(string name, string tag, string comment)> values, string typeName, string defaultValue) string defaultValue)
{ {
writer.WriteLine($" private static readonly Dictionary<string, {typeName}> s_tagTo{typeName} = "); writer.WriteLine($" private static readonly Dictionary<string, {typeName}> s_tagTo{typeName} = ");
writer.WriteLine($" new Dictionary<string,{typeName}>{{"); writer.WriteLine($" new Dictionary<string,{typeName}>{{");
foreach (var (name, tag, comment) in values) foreach (var entry in entries)
{ {
writer.WriteLine($" {{ \"{tag}\", {typeName}.{name}}},"); writer.WriteLine($" {{ \"{entry.Tag}\", {typeName}.{entry.Name.Replace("_", "")}}},");
} }
writer.WriteLine(" };"); writer.WriteLine(" };");
@ -409,4 +428,18 @@ namespace Avalonia.Visuals.UnitTests.Media.TextFormatting
writer.WriteLine(); writer.WriteLine();
} }
} }
public readonly struct DataEntry
{
public DataEntry(string name, string tag, string comment)
{
Name = name;
Tag = tag;
Comment = comment;
}
public string Name { get; }
public string Tag { get; }
public string Comment { get; }
}
} }

Loading…
Cancel
Save