Browse Source

Merge branch 'fixes/fontManagerInfiniteLoop' of https://github.com/Gillibald/Avalonia into fixes/fontManagerInfiniteLoop

pull/11775/head
Benedikt Stebner 3 years ago
parent
commit
7d35f4e9ba
  1. 32
      Avalonia.sln
  2. 2
      packages/Avalonia/Avalonia.csproj
  3. 22
      samples/ControlCatalog.NetCore/Program.cs
  4. 5
      samples/MobileSandbox.Browser/Logo.svg
  5. 43
      samples/MobileSandbox.Browser/MobileSandbox.Browser.csproj
  6. 19
      samples/MobileSandbox.Browser/Program.cs
  7. 12
      samples/MobileSandbox.Browser/Properties/launchSettings.json
  8. 7
      samples/MobileSandbox.Browser/Roots.xml
  9. 56
      samples/MobileSandbox.Browser/app.css
  10. BIN
      samples/MobileSandbox.Browser/favicon.ico
  11. 31
      samples/MobileSandbox.Browser/index.html
  12. 16
      samples/MobileSandbox.Browser/main.js
  13. 11
      samples/MobileSandbox.Browser/runtimeconfig.template.json
  14. 5
      samples/RenderDemo/Pages/CustomStringAnimator.cs
  15. 94
      src/Avalonia.Base/Animation/Animation.AnimatorRegistry.cs
  16. 59
      src/Avalonia.Base/Animation/Animation.cs
  17. 30
      src/Avalonia.Base/Animation/ICustomAnimator.cs
  18. 2
      src/Avalonia.Base/AvaloniaObjectExtensions.cs
  19. 7
      src/Avalonia.Base/CornerRadius.cs
  20. 2
      src/Avalonia.Base/Input/TouchDevice.cs
  21. 6
      src/Avalonia.Base/Media/BoxShadow.cs
  22. 6
      src/Avalonia.Base/Media/BoxShadows.cs
  23. 5
      src/Avalonia.Base/Media/Brush.cs
  24. 7
      src/Avalonia.Base/Media/Color.cs
  25. 2
      src/Avalonia.Base/Media/Effects/EffectAnimator.cs
  26. 41
      src/Avalonia.Base/Media/EllipseGeometry.cs
  27. 13
      src/Avalonia.Base/Media/GlyphRun.cs
  28. 37
      src/Avalonia.Base/Media/MediaContext.Clock.cs
  29. 3
      src/Avalonia.Base/Media/MediaContext.Compositor.cs
  30. 3
      src/Avalonia.Base/Media/MediaContext.cs
  31. 2
      src/Avalonia.Base/Media/TextFormatting/TextFormatterImpl.cs
  32. 22
      src/Avalonia.Base/Media/TextFormatting/TextLayout.cs
  33. 131
      src/Avalonia.Base/Media/TextFormatting/TextLineImpl.cs
  34. 2
      src/Avalonia.Base/Media/TextFormatting/Unicode/BiDiAlgorithm.cs
  35. 6
      src/Avalonia.Base/Media/Transform.cs
  36. 2
      src/Avalonia.Base/Platform/IPlatformRenderInterface.cs
  37. 21
      src/Avalonia.Base/Platform/Storage/FileIO/StorageProviderHelpers.cs
  38. 7
      src/Avalonia.Base/Point.cs
  39. 5
      src/Avalonia.Base/Rect.cs
  40. 7
      src/Avalonia.Base/RelativePoint.cs
  41. 4
      src/Avalonia.Base/Rendering/Composition/Compositor.cs
  42. 5
      src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionDrawListVisual.cs
  43. 25
      src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionDrawingSurface.cs
  44. 13
      src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionTarget.cs
  45. 10
      src/Avalonia.Base/Rendering/Composition/Server/ServerRenderResource.cs
  46. 7
      src/Avalonia.Base/Size.cs
  47. 7
      src/Avalonia.Base/Thickness.cs
  48. 2
      src/Avalonia.Base/Utilities/TypeUtilities.cs
  49. 7
      src/Avalonia.Base/Vector.cs
  50. 40
      src/Avalonia.Controls.ColorPicker/ColorPalettes/FlatColorPalette.cs
  51. 18
      src/Avalonia.Controls.ColorPicker/ColorSpectrum/ColorSpectrum.cs
  52. 10
      src/Avalonia.Controls.DataGrid/DataGridColumn.cs
  53. 19
      src/Avalonia.Controls/Embedding/Offscreen/OffscreenTopLevelImpl.cs
  54. 12
      src/Avalonia.Controls/Platform/ManagedDispatcherImpl.cs
  55. 1
      src/Avalonia.Controls/Primitives/OverlayLayer.cs
  56. 2
      src/Avalonia.Controls/Primitives/PopupPositioning/IPopupPositioner.cs
  57. 116
      src/Avalonia.Controls/Remote/Server/RemoteServerTopLevelImpl.Framebuffer.cs
  58. 365
      src/Avalonia.Controls/Remote/Server/RemoteServerTopLevelImpl.cs
  59. 12
      src/Avalonia.Controls/TextBlock.cs
  60. 47
      src/Avalonia.Controls/ToggleSwitch.cs
  61. 12
      src/Avalonia.Controls/TopLevel.cs
  62. 19
      src/Avalonia.Controls/TreeView.cs
  63. 7
      src/Avalonia.DesignerSupport/DesignWindowLoader.cs
  64. 12
      src/Avalonia.DesignerSupport/Remote/PreviewerWindowImpl.cs
  65. 2
      src/Avalonia.DesignerSupport/Remote/PreviewerWindowingPlatform.cs
  66. 9
      src/Avalonia.DesignerSupport/Remote/RemoteDesignerEntryPoint.cs
  67. 21
      src/Avalonia.Diagnostics/Diagnostics/Controls/BrushEditor.axaml
  68. 121
      src/Avalonia.Diagnostics/Diagnostics/Controls/BrushEditor.axaml.cs
  69. 94
      src/Avalonia.Diagnostics/Diagnostics/Controls/BrushEditor.cs
  70. 1
      src/Avalonia.Diagnostics/Diagnostics/ViewModels/MainViewModel.cs
  71. 1
      src/Avalonia.Diagnostics/Diagnostics/Views/MainWindow.xaml
  72. 20
      src/Avalonia.Themes.Fluent/Controls/ToggleSwitch.xaml
  73. 13
      src/Avalonia.Themes.Simple/Controls/ToggleSwitch.xaml
  74. 21
      src/Linux/Avalonia.LinuxFramebuffer/LinuxFramebufferPlatform.cs
  75. 2
      src/tools/Avalonia.Designer.HostApp/Avalonia.Designer.HostApp.csproj
  76. 33
      tests/Avalonia.Controls.UnitTests/TextBlockTests.cs
  77. 15
      tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextLayoutTests.cs
  78. 33
      tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextLineTests.cs

32
Avalonia.sln

@ -264,13 +264,15 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Headless", "Headless", "{FF
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Avalonia.Headless.XUnit", "src\Headless\Avalonia.Headless.XUnit\Avalonia.Headless.XUnit.csproj", "{F47F8316-4D4B-4026-8EF3-16B2CFDA8119}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Avalonia.Headless.NUnit", "src\Headless\Avalonia.Headless.NUnit\Avalonia.Headless.NUnit.csproj", "{ED976634-B118-43F8-8B26-0279C7A7044F}"
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Avalonia.Headless.NUnit", "src\Headless\Avalonia.Headless.NUnit\Avalonia.Headless.NUnit.csproj", "{ED976634-B118-43F8-8B26-0279C7A7044F}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Avalonia.Generators.Tests", "tests\Avalonia.Generators.Tests\Avalonia.Generators.Tests.csproj", "{4B8EBBEB-A1AD-49EC-8B69-B93ED15BFA64}"
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Avalonia.Generators.Tests", "tests\Avalonia.Generators.Tests\Avalonia.Generators.Tests.csproj", "{4B8EBBEB-A1AD-49EC-8B69-B93ED15BFA64}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Avalonia.Headless.NUnit.UnitTests", "tests\Avalonia.Headless.NUnit.UnitTests\Avalonia.Headless.NUnit.UnitTests.csproj", "{2999D79E-3C20-4A90-B651-CA7E0AC92D35}"
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Avalonia.Headless.NUnit.UnitTests", "tests\Avalonia.Headless.NUnit.UnitTests\Avalonia.Headless.NUnit.UnitTests.csproj", "{2999D79E-3C20-4A90-B651-CA7E0AC92D35}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Avalonia.Headless.XUnit.UnitTests", "tests\Avalonia.Headless.XUnit.UnitTests\Avalonia.Headless.XUnit.UnitTests.csproj", "{F83FC908-A4E3-40DE-B4CF-A4BA1E92CDB3}"
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Avalonia.Headless.XUnit.UnitTests", "tests\Avalonia.Headless.XUnit.UnitTests\Avalonia.Headless.XUnit.UnitTests.csproj", "{F83FC908-A4E3-40DE-B4CF-A4BA1E92CDB3}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MobileSandbox.Browser", "samples\MobileSandbox.Browser\MobileSandbox.Browser.csproj", "{43FCC14E-EEBE-44B3-BCBC-F1C537EECBF8}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
@ -607,10 +609,6 @@ Global
{13F1135D-BA1A-435C-9C5B-A368D1D63DE4}.Debug|Any CPU.Build.0 = Debug|Any CPU
{13F1135D-BA1A-435C-9C5B-A368D1D63DE4}.Release|Any CPU.ActiveCfg = Release|Any CPU
{13F1135D-BA1A-435C-9C5B-A368D1D63DE4}.Release|Any CPU.Build.0 = Release|Any CPU
{F47F8316-4D4B-4026-8EF3-16B2CFDA8119}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{F47F8316-4D4B-4026-8EF3-16B2CFDA8119}.Debug|Any CPU.Build.0 = Debug|Any CPU
{F47F8316-4D4B-4026-8EF3-16B2CFDA8119}.Release|Any CPU.ActiveCfg = Release|Any CPU
{F47F8316-4D4B-4026-8EF3-16B2CFDA8119}.Release|Any CPU.Build.0 = Release|Any CPU
{A82AD1BC-EBE6-4FC3-A13B-D52A50297533}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{A82AD1BC-EBE6-4FC3-A13B-D52A50297533}.Debug|Any CPU.Build.0 = Debug|Any CPU
{A82AD1BC-EBE6-4FC3-A13B-D52A50297533}.Release|Any CPU.ActiveCfg = Release|Any CPU
@ -639,14 +637,14 @@ Global
{FC956F9A-4C3A-4A1A-ACDD-BB54DCB661DD}.Release|Any CPU.ActiveCfg = Release|Any CPU
{FC956F9A-4C3A-4A1A-ACDD-BB54DCB661DD}.Release|Any CPU.Build.0 = Release|Any CPU
{FC956F9A-4C3A-4A1A-ACDD-BB54DCB661DD}.Release|Any CPU.Deploy.0 = Release|Any CPU
{ED976634-B118-43F8-8B26-0279C7A7044F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{ED976634-B118-43F8-8B26-0279C7A7044F}.Debug|Any CPU.Build.0 = Debug|Any CPU
{ED976634-B118-43F8-8B26-0279C7A7044F}.Release|Any CPU.ActiveCfg = Release|Any CPU
{ED976634-B118-43F8-8B26-0279C7A7044F}.Release|Any CPU.Build.0 = Release|Any CPU
{F47F8316-4D4B-4026-8EF3-16B2CFDA8119}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{F47F8316-4D4B-4026-8EF3-16B2CFDA8119}.Debug|Any CPU.Build.0 = Debug|Any CPU
{F47F8316-4D4B-4026-8EF3-16B2CFDA8119}.Release|Any CPU.ActiveCfg = Release|Any CPU
{F47F8316-4D4B-4026-8EF3-16B2CFDA8119}.Release|Any CPU.Build.0 = Release|Any CPU
{ED976634-B118-43F8-8B26-0279C7A7044F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{ED976634-B118-43F8-8B26-0279C7A7044F}.Debug|Any CPU.Build.0 = Debug|Any CPU
{ED976634-B118-43F8-8B26-0279C7A7044F}.Release|Any CPU.ActiveCfg = Release|Any CPU
{ED976634-B118-43F8-8B26-0279C7A7044F}.Release|Any CPU.Build.0 = Release|Any CPU
{4B8EBBEB-A1AD-49EC-8B69-B93ED15BFA64}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{4B8EBBEB-A1AD-49EC-8B69-B93ED15BFA64}.Debug|Any CPU.Build.0 = Debug|Any CPU
{4B8EBBEB-A1AD-49EC-8B69-B93ED15BFA64}.Release|Any CPU.ActiveCfg = Release|Any CPU
@ -659,6 +657,10 @@ Global
{F83FC908-A4E3-40DE-B4CF-A4BA1E92CDB3}.Debug|Any CPU.Build.0 = Debug|Any CPU
{F83FC908-A4E3-40DE-B4CF-A4BA1E92CDB3}.Release|Any CPU.ActiveCfg = Release|Any CPU
{F83FC908-A4E3-40DE-B4CF-A4BA1E92CDB3}.Release|Any CPU.Build.0 = Release|Any CPU
{43FCC14E-EEBE-44B3-BCBC-F1C537EECBF8}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{43FCC14E-EEBE-44B3-BCBC-F1C537EECBF8}.Debug|Any CPU.Build.0 = Debug|Any CPU
{43FCC14E-EEBE-44B3-BCBC-F1C537EECBF8}.Release|Any CPU.ActiveCfg = Release|Any CPU
{43FCC14E-EEBE-44B3-BCBC-F1C537EECBF8}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
@ -728,9 +730,6 @@ Global
{C810060E-3809-4B74-A125-F11533AF9C1B} = {9B9E3891-2366-4253-A952-D08BCEB71098}
{C692FE73-43DB-49CE-87FC-F03ED61F25C9} = {4ED8B739-6F4E-4CD4-B993-545E6B5CE637}
{F4E36AA8-814E-4704-BC07-291F70F45193} = {C5A00AC3-B34C-4564-9BDD-2DA473EF4D8B}
{8C89950F-F5D9-47FC-8066-CBC1EC3DF8FC} = {FF237916-7150-496B-89ED-6CA3292896E7}
{B859AE7C-F34F-4A9E-88AE-E0E7229FDE1E} = {FF237916-7150-496B-89ED-6CA3292896E7}
{F47F8316-4D4B-4026-8EF3-16B2CFDA8119} = {FF237916-7150-496B-89ED-6CA3292896E7}
{DDA28789-C21A-4654-86CE-D01E81F095C5} = {4ED8B739-6F4E-4CD4-B993-545E6B5CE637}
{A82AD1BC-EBE6-4FC3-A13B-D52A50297533} = {9B9E3891-2366-4253-A952-D08BCEB71098}
{F8928267-688E-4A51-989C-612A72446D33} = {9B9E3891-2366-4253-A952-D08BCEB71098}
@ -738,11 +737,12 @@ Global
{22E3BC08-EAF7-4889-BDC4-B4D3046C4E2D} = {9B9E3891-2366-4253-A952-D08BCEB71098}
{4CDAD037-34A2-4CCF-A03A-C6C7B988A572} = {9B9E3891-2366-4253-A952-D08BCEB71098}
{FC956F9A-4C3A-4A1A-ACDD-BB54DCB661DD} = {9B9E3891-2366-4253-A952-D08BCEB71098}
{ED976634-B118-43F8-8B26-0279C7A7044F} = {FF237916-7150-496B-89ED-6CA3292896E7}
{F47F8316-4D4B-4026-8EF3-16B2CFDA8119} = {FF237916-7150-496B-89ED-6CA3292896E7}
{ED976634-B118-43F8-8B26-0279C7A7044F} = {FF237916-7150-496B-89ED-6CA3292896E7}
{4B8EBBEB-A1AD-49EC-8B69-B93ED15BFA64} = {C5A00AC3-B34C-4564-9BDD-2DA473EF4D8B}
{2999D79E-3C20-4A90-B651-CA7E0AC92D35} = {C5A00AC3-B34C-4564-9BDD-2DA473EF4D8B}
{F83FC908-A4E3-40DE-B4CF-A4BA1E92CDB3} = {C5A00AC3-B34C-4564-9BDD-2DA473EF4D8B}
{43FCC14E-EEBE-44B3-BCBC-F1C537EECBF8} = {9B9E3891-2366-4253-A952-D08BCEB71098}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {87366D66-1391-4D90-8999-95A620AD786A}

2
packages/Avalonia/Avalonia.csproj

@ -5,7 +5,7 @@
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Avalonia.BuildServices" Version="0.0.19" />
<PackageReference Include="Avalonia.BuildServices" Version="0.0.22" />
<ProjectReference Include="../../src/Avalonia.Remote.Protocol/Avalonia.Remote.Protocol.csproj" />
<ProjectReference Include="../../src/Avalonia.Build.Tasks/Avalonia.Build.Tasks.csproj">
<PrivateAssets>all</PrivateAssets>

22
samples/ControlCatalog.NetCore/Program.cs

@ -18,9 +18,16 @@ namespace ControlCatalog.NetCore
{
static class Program
{
private static bool s_useFramebuffer;
[STAThread]
static int Main(string[] args)
{
if (args.Contains("--fbdev"))
{
s_useFramebuffer = true;
}
if (args.Contains("--wait-for-attach"))
{
Console.WriteLine("Attach debugger and use 'Set next statement'");
@ -42,10 +49,10 @@ namespace ControlCatalog.NetCore
return scaling;
return 1;
}
if (args.Contains("--fbdev"))
if (s_useFramebuffer)
{
SilenceConsole();
return builder.StartLinuxFbDev(args, scaling: GetScaling());
SilenceConsole();
return builder.StartLinuxFbDev(args, scaling: GetScaling());
}
else if (args.Contains("--vnc"))
{
@ -128,10 +135,13 @@ namespace ControlCatalog.NetCore
.WithInterFont()
.AfterSetup(builder =>
{
builder.Instance!.AttachDevTools(new Avalonia.Diagnostics.DevToolsOptions()
if (!s_useFramebuffer)
{
StartupScreenIndex = 1,
});
builder.Instance!.AttachDevTools(new Avalonia.Diagnostics.DevToolsOptions()
{
StartupScreenIndex = 1,
});
}
EmbedSample.Implementation = OperatingSystem.IsWindows() ? (INativeDemoControl)new EmbedSampleWin()
: OperatingSystem.IsMacOS() ? new EmbedSampleMac()

5
samples/MobileSandbox.Browser/Logo.svg

@ -0,0 +1,5 @@
<svg width="35" height="35" viewBox="0 0 35 35" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M30.4661 34.928C30.5364 34.928 30.6052 34.928 30.6754 34.928C32.8596 34.928 34.654 33.2918 34.9053 31.1752L34.9356 16.9955C34.6872 7.56697 26.9662 0 17.4777 0C7.83263 0 0.0137329 7.8189 0.0137329 17.464C0.0137329 27.0059 7.66618 34.7631 17.1687 34.928H30.4661Z" fill="white"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M17.5239 5.948C12.0268 5.948 7.42967 9.80117 6.286 14.954C7.38092 15.2609 8.18385 16.2664 8.18385 17.4593C8.18385 18.6523 7.38092 19.6577 6.286 19.9647C7.42966 25.1175 12.0268 28.9706 17.5239 28.9706C19.525 28.9706 21.4068 28.4601 23.0462 27.562V28.8927H29.0352V17.9365C29.0407 17.7908 29.0352 17.6063 29.0352 17.4593C29.0352 11.1018 23.8814 5.948 17.5239 5.948ZM12.0098 17.4593C12.0098 14.414 14.4786 11.9452 17.5239 11.9452C20.5693 11.9452 23.038 14.414 23.038 17.4593C23.038 20.5047 20.5693 22.9734 17.5239 22.9734C14.4786 22.9734 12.0098 20.5047 12.0098 17.4593Z" fill="#8B44AC"/>
<path d="M7.36841 17.4517C7.36841 18.4691 6.54368 19.2938 5.52631 19.2938C4.50894 19.2938 3.6842 18.4691 3.6842 17.4517C3.6842 16.4343 4.50894 15.6096 5.52631 15.6096C6.54368 15.6096 7.36841 16.4343 7.36841 17.4517Z" fill="#8B44AC"/>
</svg>

After

Width:  |  Height:  |  Size: 1.2 KiB

43
samples/MobileSandbox.Browser/MobileSandbox.Browser.csproj

@ -0,0 +1,43 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net7.0</TargetFramework>
<RuntimeIdentifier>browser-wasm</RuntimeIdentifier>
<WasmMainJSPath>main.js</WasmMainJSPath>
<OutputType>Exe</OutputType>
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
<MSBuildEnableWorkloadResolver>true</MSBuildEnableWorkloadResolver>
<WasmBuildNative>true</WasmBuildNative>
<EmccFlags>-sVERBOSE -sERROR_ON_UNDEFINED_SYMBOLS=0</EmccFlags>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)'=='Release'">
<RunAOTCompilation>true</RunAOTCompilation>
<PublishTrimmed>true</PublishTrimmed>
<TrimMode>full</TrimMode>
<WasmBuildNative>true</WasmBuildNative>
<InvariantGlobalization>true</InvariantGlobalization>
<EmccCompileOptimizationFlag>-O2</EmccCompileOptimizationFlag>
<EmccLinkOptimizationFlag>-O2</EmccLinkOptimizationFlag>
</PropertyGroup>
<ItemGroup>
<TrimmerRootDescriptor Include="Roots.xml" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\src\Skia\Avalonia.Skia\Avalonia.Skia.csproj" />
<ProjectReference Include="..\..\src\Browser\Avalonia.Browser\Avalonia.Browser.csproj" />
<ProjectReference Include="..\MobileSandbox\MobileSandbox.csproj" />
</ItemGroup>
<ItemGroup>
<WasmExtraFilesToDeploy Include="index.html" />
<WasmExtraFilesToDeploy Include="main.js" />
<WasmExtraFilesToDeploy Include="favicon.ico" />
<WasmExtraFilesToDeploy Include="Logo.svg" />
<WasmExtraFilesToDeploy Include="app.css" />
</ItemGroup>
<Import Project="..\..\src\Browser\Avalonia.Browser\Avalonia.Browser.props" />
<Import Project="..\..\src\Browser\Avalonia.Browser\Avalonia.Browser.targets" />
</Project>

19
samples/MobileSandbox.Browser/Program.cs

@ -0,0 +1,19 @@
using System.Runtime.Versioning;
using System.Threading.Tasks;
using Avalonia;
using Avalonia.Browser;
using MobileSandbox;
[assembly:SupportedOSPlatform("browser")]
internal partial class Program
{
public static async Task Main(string[] args)
{
await BuildAvaloniaApp()
.StartBrowserAppAsync("out");
}
public static AppBuilder BuildAvaloniaApp()
=> AppBuilder.Configure<App>();
}

12
samples/MobileSandbox.Browser/Properties/launchSettings.json

@ -0,0 +1,12 @@
{
"profiles": {
"MobileSandbox.Browser": {
"commandName": "Project",
"launchBrowser": true,
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
},
"applicationUrl": "https://localhost:65312;http://localhost:65313;"
}
}
}

7
samples/MobileSandbox.Browser/Roots.xml

@ -0,0 +1,7 @@
<linker>
<assembly fullname="MobileSandbox" preserve="All" />
<assembly fullname="MobileSandbox.Browser" preserve="All" />
<assembly fullname="Avalonia.Themes.Fluent" preserve="All" />
<assembly fullname="Avalonia.Themes.Simple" preserve="All" />
<assembly fullname="Avalonia.Controls.ColorPicker" preserve="All" />
</linker>

56
samples/MobileSandbox.Browser/app.css

@ -0,0 +1,56 @@
:root {
--sat: env(safe-area-inset-top);
--sar: env(safe-area-inset-right);
--sab: env(safe-area-inset-bottom);
--sal: env(safe-area-inset-left);
}
#out {
height: 100vh;
width: 100vw
}
#avalonia-splash {
position: relative;
height: 100%;
width: 100%;
color: whitesmoke;
background: #171C2C;
font-family: 'Nunito', sans-serif;
background-position: center;
background-size: cover;
background-repeat: no-repeat;
}
#avalonia-splash a{
color: whitesmoke;
text-decoration: none;
}
.center {
display: flex;
justify-content: center;
height: 250px;
}
.splash-close {
animation: slide 0.5s linear 1s forwards;
}
@keyframes slide {
0% {
top: 0%;
}
50% {
opacity: 80%;
}
100% {
top: 100%;
overflow: hidden;
opacity: 0;
display: none;
visibility: collapse;
}
}

BIN
samples/MobileSandbox.Browser/favicon.ico

Binary file not shown.

After

Width:  |  Height:  |  Size: 172 KiB

31
samples/MobileSandbox.Browser/index.html

@ -0,0 +1,31 @@
<!DOCTYPE html>
<!-- Licensed to the .NET Foundation under one or more agreements. -->
<!-- The .NET Foundation licenses this file to you under the MIT license. -->
<html>
<head>
<title>Mobile Sandbox</title>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<link rel="modulepreload" href="./main.js" />
<link rel="modulepreload" href="./dotnet.js" />
<link rel="modulepreload" href="./avalonia.js" />
<link rel="stylesheet" href="./app.css" />
</head>
<body style="margin: 0px">
<div id="out">
<div id="avalonia-splash">
<div class="center">
<h2>Powered by</h2>
<a class="navbar-brand" href="https://www.avaloniaui.net/" target="_blank">
<img src="Logo.svg" alt="Avalonia Logo" width="30" height="24" />
Avalonia
</a>
</div>
</div>
</div>
<script type='module' src="./main.js"></script>
</body>
</html>

16
samples/MobileSandbox.Browser/main.js

@ -0,0 +1,16 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
import { dotnet } from './dotnet.js'
const is_browser = typeof window != "undefined";
if (!is_browser) throw new Error(`Expected to be running in a browser`);
const dotnetRuntime = await dotnet
.withDiagnosticTracing(false)
.withApplicationArgumentsFromQuery()
.create();
const config = dotnetRuntime.getConfig();
await dotnetRuntime.runMainAndExit(config.mainAssemblyName, ["dotnet", "is", "great!"]);

11
samples/MobileSandbox.Browser/runtimeconfig.template.json

@ -0,0 +1,11 @@
{
"wasmHostProperties": {
"perHostConfig": [
{
"name": "browser",
"html-path": "index.html",
"Host": "browser"
}
]
}
}

5
samples/RenderDemo/Pages/CustomStringAnimator.cs

@ -1,9 +1,10 @@
using Avalonia.Animation;
using System;
using Avalonia.Animation;
using Avalonia.Animation.Animators;
namespace RenderDemo.Pages
{
public class CustomStringAnimator : CustomAnimatorBase<string>
public class CustomStringAnimator : InterpolatingAnimator<string>
{
public override string Interpolate(double progress, string oldValue, string newValue)
{

94
src/Avalonia.Base/Animation/Animation.AnimatorRegistry.cs

@ -0,0 +1,94 @@
using System;
using System.Collections.Generic;
using Avalonia.Animation.Animators;
using Avalonia.Media;
namespace Avalonia.Animation;
partial class Animation
{
/// <summary>
/// Sets the value of the Animator attached property for a setter.
/// </summary>
/// <param name="setter">The animation setter.</param>
/// <param name="value">The property animator value.</param>
[Obsolete("CustomAnimatorBase will be removed before 11.0, use InterpolatingAnimator<T>", true)]
public static void SetAnimator(IAnimationSetter setter, CustomAnimatorBase value)
{
s_animators[setter] = (value.WrapperType, value.CreateWrapper);
}
/// <summary>
/// Sets the value of the Animator attached property for a setter.
/// </summary>
/// <param name="setter">The animation setter.</param>
/// <param name="value">The property animator value.</param>
public static void SetAnimator(IAnimationSetter setter, ICustomAnimator value)
{
s_animators[setter] = (value.WrapperType, value.CreateWrapper);
}
private readonly static List<(Func<AvaloniaProperty, bool> Condition, Type Animator, Func<IAnimator> Factory)>
Animators = new()
{
(prop =>(typeof(double).IsAssignableFrom(prop.PropertyType) && typeof(Transform).IsAssignableFrom(prop.OwnerType)),
typeof(TransformAnimator), () => new TransformAnimator()),
(prop => typeof(bool).IsAssignableFrom(prop.PropertyType), typeof(BoolAnimator), () => new BoolAnimator()),
(prop => typeof(byte).IsAssignableFrom(prop.PropertyType), typeof(ByteAnimator), () => new ByteAnimator()),
(prop => typeof(Int16).IsAssignableFrom(prop.PropertyType), typeof(Int16Animator), () => new Int16Animator()),
(prop => typeof(Int32).IsAssignableFrom(prop.PropertyType), typeof(Int32Animator), () => new Int32Animator()),
(prop => typeof(Int64).IsAssignableFrom(prop.PropertyType), typeof(Int64Animator), () => new Int64Animator()),
(prop => typeof(UInt16).IsAssignableFrom(prop.PropertyType), typeof(UInt16Animator), () => new UInt16Animator()),
(prop => typeof(UInt32).IsAssignableFrom(prop.PropertyType), typeof(UInt32Animator), () => new UInt32Animator()),
(prop => typeof(UInt64).IsAssignableFrom(prop.PropertyType), typeof(UInt64Animator), () => new UInt64Animator()),
(prop => typeof(float).IsAssignableFrom(prop.PropertyType), typeof(FloatAnimator), () => new FloatAnimator()),
(prop => typeof(double).IsAssignableFrom(prop.PropertyType), typeof(DoubleAnimator), () => new DoubleAnimator()),
(prop => typeof(decimal).IsAssignableFrom(prop.PropertyType), typeof(DecimalAnimator), () => new DecimalAnimator()),
};
static Animation()
{
RegisterAnimator<IEffect?, EffectAnimator>();
RegisterAnimator<BoxShadow, BoxShadowAnimator>();
RegisterAnimator<BoxShadows, BoxShadowsAnimator>();
RegisterAnimator<IBrush?, BaseBrushAnimator>();
RegisterAnimator<CornerRadius, CornerRadiusAnimator>();
RegisterAnimator<Color, ColorAnimator>();
RegisterAnimator<Vector, VectorAnimator>();
RegisterAnimator<Point, PointAnimator>();
RegisterAnimator<Rect, RectAnimator>();
RegisterAnimator<RelativePoint, RelativePointAnimator>();
RegisterAnimator<Size, SizeAnimator>();
RegisterAnimator<Thickness, ThicknessAnimator>();
}
/// <summary>
/// Registers a <see cref="Animator{T}"/> that can handle
/// a value type that matches the specified condition.
/// </summary>
static void RegisterAnimator<T, TAnimator>()
where TAnimator : Animator<T>, new()
{
Animators.Insert(0,
(prop => typeof(T).IsAssignableFrom(prop.PropertyType), typeof(TAnimator), () => new TAnimator()));
}
public static void RegisterCustomAnimator<T, TAnimator>() where TAnimator : InterpolatingAnimator<T>, new()
{
Animators.Insert(0, (prop => typeof(T).IsAssignableFrom(prop.PropertyType),
typeof(InterpolatingAnimator<T>.AnimatorWrapper), () => new TAnimator().CreateWrapper()));
}
private static (Type Type, Func<IAnimator> Factory)? GetAnimatorType(AvaloniaProperty property)
{
foreach (var (condition, type, factory) in Animators)
{
if (condition(property))
{
return (type, factory);
}
}
return null;
}
}

59
src/Avalonia.Base/Animation/Animation.cs

@ -1,12 +1,9 @@
using System;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using Avalonia.Reactive;
using System.Threading;
using System.Threading.Tasks;
using Avalonia.Animation.Animators;
using Avalonia.Animation.Easings;
using Avalonia.Data;
using Avalonia.Metadata;
@ -16,7 +13,7 @@ namespace Avalonia.Animation
/// <summary>
/// Tracks the progress of an animation.
/// </summary>
public sealed class Animation : AvaloniaObject, IAnimation
public sealed partial class Animation : AvaloniaObject, IAnimation
{
/// <summary>
/// Defines the <see cref="Duration"/> property.
@ -195,60 +192,6 @@ namespace Avalonia.Animation
return null;
}
/// <summary>
/// Sets the value of the Animator attached property for a setter.
/// </summary>
/// <param name="setter">The animation setter.</param>
/// <param name="value">The property animator value.</param>
public static void SetAnimator(IAnimationSetter setter, CustomAnimatorBase value)
{
s_animators[setter] = (value.WrapperType, value.CreateWrapper);
}
private readonly static List<(Func<AvaloniaProperty, bool> Condition, Type Animator, Func<IAnimator> Factory)> Animators = new()
{
( prop => typeof(bool).IsAssignableFrom(prop.PropertyType), typeof(BoolAnimator), () => new BoolAnimator() ),
( prop => typeof(byte).IsAssignableFrom(prop.PropertyType), typeof(ByteAnimator), () => new ByteAnimator() ),
( prop => typeof(Int16).IsAssignableFrom(prop.PropertyType), typeof(Int16Animator), () => new Int16Animator() ),
( prop => typeof(Int32).IsAssignableFrom(prop.PropertyType), typeof(Int32Animator), () => new Int32Animator() ),
( prop => typeof(Int64).IsAssignableFrom(prop.PropertyType), typeof(Int64Animator), () => new Int64Animator() ),
( prop => typeof(UInt16).IsAssignableFrom(prop.PropertyType), typeof(UInt16Animator), () => new UInt16Animator() ),
( prop => typeof(UInt32).IsAssignableFrom(prop.PropertyType), typeof(UInt32Animator), () => new UInt32Animator() ),
( prop => typeof(UInt64).IsAssignableFrom(prop.PropertyType), typeof(UInt64Animator), () => new UInt64Animator() ),
( prop => typeof(float).IsAssignableFrom(prop.PropertyType), typeof(FloatAnimator), () => new FloatAnimator() ),
( prop => typeof(double).IsAssignableFrom(prop.PropertyType), typeof(DoubleAnimator), () => new DoubleAnimator() ),
( prop => typeof(decimal).IsAssignableFrom(prop.PropertyType), typeof(DecimalAnimator), () => new DecimalAnimator() ),
};
/// <summary>
/// Registers a <see cref="Animator{T}"/> that can handle
/// a value type that matches the specified condition.
/// </summary>
/// <param name="condition">
/// The condition to which the <see cref="Animator{T}"/>
/// is to be activated and used.
/// </param>
/// <typeparam name="TAnimator">
/// The type of the animator to instantiate.
/// </typeparam>
internal static void RegisterAnimator<TAnimator>(Func<AvaloniaProperty, bool> condition)
where TAnimator : IAnimator, new()
{
Animators.Insert(0, (condition, typeof(TAnimator), () => new TAnimator()));
}
private static (Type Type, Func<IAnimator> Factory)? GetAnimatorType(AvaloniaProperty property)
{
foreach (var (condition, type, factory) in Animators)
{
if (condition(property))
{
return (type, factory);
}
}
return null;
}
private (IList<IAnimator> Animators, IList<IDisposable> subscriptions) InterpretKeyframes(Animatable control)
{
var handlerList = new Dictionary<(Type type, AvaloniaProperty Property), Func<IAnimator>>();

30
src/Avalonia.Base/Animation/ICustomAnimator.cs

@ -1,14 +1,15 @@
using System;
using Avalonia.Animation.Animators;
namespace Avalonia.Animation;
[Obsolete("This class will be removed before 11.0, use InterpolatingAnimator<T>", true)]
public abstract class CustomAnimatorBase
{
internal abstract IAnimator CreateWrapper();
internal abstract Type WrapperType { get; }
}
[Obsolete("This class will be removed before 11.0, use InterpolatingAnimator<T>", true)]
public abstract class CustomAnimatorBase<T> : CustomAnimatorBase
{
public abstract T Interpolate(double progress, T oldValue, T newValue);
@ -25,6 +26,33 @@ public abstract class CustomAnimatorBase<T> : CustomAnimatorBase
_parent = parent;
}
public override T Interpolate(double progress, T oldValue, T newValue) => _parent.Interpolate(progress, oldValue, newValue);
}
}
public interface ICustomAnimator
{
internal IAnimator CreateWrapper();
internal Type WrapperType { get; }
}
public abstract class InterpolatingAnimator<T> : ICustomAnimator
{
public abstract T Interpolate(double progress, T oldValue, T newValue);
Type ICustomAnimator.WrapperType => typeof(AnimatorWrapper);
IAnimator ICustomAnimator.CreateWrapper() => new AnimatorWrapper(this);
internal IAnimator CreateWrapper() => new AnimatorWrapper(this);
internal class AnimatorWrapper : Animator<T>
{
private readonly InterpolatingAnimator<T> _parent;
public AnimatorWrapper(InterpolatingAnimator<T> parent)
{
_parent = parent;
}
public override T Interpolate(double progress, T oldValue, T newValue) => _parent.Interpolate(progress, oldValue, newValue);
}
}

2
src/Avalonia.Base/AvaloniaObjectExtensions.cs

@ -334,7 +334,7 @@ namespace Avalonia
/// <typeparamref name="TTarget"/>.
/// </summary>
/// <typeparam name="TTarget">The type of the property change sender.</typeparam>
/// /// <typeparam name="TValue">The type of the property..</typeparam>
/// <typeparam name="TValue">The type of the property.</typeparam>
/// <param name="observable">The property changed observable.</param>
/// <param name="action">
/// The method to call. The parameters are the sender and the event args.

7
src/Avalonia.Base/CornerRadius.cs

@ -15,13 +15,6 @@ namespace Avalonia
#endif
readonly struct CornerRadius : IEquatable<CornerRadius>
{
static CornerRadius()
{
#if !BUILDTASK
Animation.Animation.RegisterAnimator<CornerRadiusAnimator>(prop => typeof(CornerRadius).IsAssignableFrom(prop.PropertyType));
#endif
}
public CornerRadius(double uniformRadius)
{
TopLeft = TopRight = BottomLeft = BottomRight = uniformRadius;

2
src/Avalonia.Base/Input/TouchDevice.cs

@ -51,7 +51,7 @@ namespace Avalonia.Input
pointer.Capture(hit);
}
var target = pointer.Captured ?? args.Root;
var target = pointer.Captured ?? args.InputHitTestResult ?? args.Root;
var gestureTarget = pointer.CapturedGestureRecognizer?.Target;
var updateKind = args.Type.ToUpdateKind();
var keyModifier = args.InputModifiers.ToKeyModifiers();

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

@ -16,12 +16,6 @@ namespace Avalonia.Media
public Color Color { get; set; }
public bool IsInset { get; set; }
static BoxShadow()
{
Animation.Animation.RegisterAnimator<BoxShadowAnimator>(prop =>
typeof(BoxShadow).IsAssignableFrom(prop.PropertyType));
}
public bool Equals(in BoxShadow other)
{
return OffsetX.Equals(other.OffsetX) && OffsetY.Equals(other.OffsetY) && Blur.Equals(other.Blur) && Spread.Equals(other.Spread) && Color.Equals(other.Color);

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

@ -10,12 +10,6 @@ namespace Avalonia.Media
private readonly BoxShadow _first;
private readonly BoxShadow[]? _list;
public int Count { get; }
static BoxShadows()
{
Animation.Animation.RegisterAnimator<BoxShadowsAnimator>(prop =>
typeof(BoxShadows).IsAssignableFrom(prop.PropertyType));
}
public BoxShadows(BoxShadow shadow)
{

5
src/Avalonia.Base/Media/Brush.cs

@ -34,11 +34,6 @@ namespace Avalonia.Media
/// </summary>
public static readonly StyledProperty<RelativePoint> TransformOriginProperty =
AvaloniaProperty.Register<Brush, RelativePoint>(nameof(TransformOrigin));
static Brush()
{
Animation.Animation.RegisterAnimator<BaseBrushAnimator>(prop => typeof(IBrush).IsAssignableFrom(prop.PropertyType));
}
/// <summary>
/// Gets or sets the opacity of the brush.

7
src/Avalonia.Base/Media/Color.cs

@ -25,13 +25,6 @@ namespace Avalonia.Media
{
private const double byteToDouble = 1.0 / 255;
static Color()
{
#if !BUILDTASK
Animation.Animation.RegisterAnimator<ColorAnimator>(prop => typeof(Color).IsAssignableFrom(prop.PropertyType));
#endif
}
/// <summary>
/// Gets the Alpha component of the color.
/// </summary>

2
src/Avalonia.Base/Media/Effects/EffectAnimator.cs

@ -63,8 +63,6 @@ internal class EffectAnimator : Animator<IEffect?>
if(s_Registered)
return;
s_Registered = true;
Animation.RegisterAnimator<EffectAnimator>(prop =>
typeof(IEffect).IsAssignableFrom(prop.PropertyType));
}
}

41
src/Avalonia.Base/Media/EllipseGeometry.cs

@ -56,6 +56,10 @@ namespace Avalonia.Media
/// <summary>
/// Gets or sets a rect that defines the bounds of the ellipse.
/// </summary>
/// <remarks>
/// When set, this takes priority over the other properties that define an
/// ellipse using a center point and X/Y-axis radii.
/// </remarks>
public Rect Rect
{
get => GetValue(RectProperty);
@ -65,6 +69,10 @@ namespace Avalonia.Media
/// <summary>
/// Gets or sets a double that defines the radius in the X-axis of the ellipse.
/// </summary>
/// <remarks>
/// In order for this property to be used, <see cref="Rect"/> must not be set
/// (equal to the default <see cref="Avalonia.Rect"/> value).
/// </remarks>
public double RadiusX
{
get => GetValue(RadiusXProperty);
@ -74,6 +82,10 @@ namespace Avalonia.Media
/// <summary>
/// Gets or sets a double that defines the radius in the Y-axis of the ellipse.
/// </summary>
/// <remarks>
/// In order for this property to be used, <see cref="Rect"/> must not be set
/// (equal to the default <see cref="Avalonia.Rect"/> value).
/// </remarks>
public double RadiusY
{
get => GetValue(RadiusYProperty);
@ -83,6 +95,10 @@ namespace Avalonia.Media
/// <summary>
/// Gets or sets a point that defines the center of the ellipse.
/// </summary>
/// <remarks>
/// In order for this property to be used, <see cref="Rect"/> must not be set
/// (equal to the default <see cref="Avalonia.Rect"/> value).
/// </remarks>
public Point Center
{
get => GetValue(CenterProperty);
@ -92,7 +108,30 @@ namespace Avalonia.Media
/// <inheritdoc/>
public override Geometry Clone()
{
return new EllipseGeometry(Rect);
// Note that the ellipse properties are used in two modes:
//
// 1. Rect-only Mode:
// Directly set the rectangle bounds the ellipse will fill
//
// 2. Center + Radii Mode:
// Set a center-point and then X/Y-axis radii that are used to
// calculate the rectangle bounds the ellipse will fill.
// This is the only mode supported by WPF.
//
// Rendering the ellipse will only ever use one of these two modes
// based on if the Rect property is set (not equal to default).
//
// This means it would normally be fine to copy ONLY the Rect property
// when it is set. However, while it would render the same, it isn't
// a true clone. We want to include all the properties here regardless
// of the rendering mode that will eventually be used.
return new EllipseGeometry()
{
Rect = Rect,
RadiusX = RadiusX,
RadiusY = RadiusY,
Center = Center,
};
}
/// <inheritdoc/>

13
src/Avalonia.Base/Media/GlyphRun.cs

@ -643,12 +643,13 @@ namespace Avalonia.Media
lastCluster = _glyphInfos[_glyphInfos.Count - 1].GlyphCluster;
}
var isReversed = firstCluster > lastCluster;
if (!IsLeftToRight)
{
(lastCluster, firstCluster) = (firstCluster, lastCluster);
}
var isReversed = firstCluster > lastCluster;
var height = GlyphTypeface.Metrics.LineSpacing * Scale;
var widthIncludingTrailingWhitespace = 0d;
@ -766,15 +767,13 @@ namespace Avalonia.Media
if (!charactersSpan.IsEmpty)
{
var characterIndex = 0;
var characterIndex = charactersSpan.Length - 1;
for (var i = 0; i < _glyphInfos.Count; i++)
{
var currentCluster = _glyphInfos[i].GlyphCluster;
var codepoint = Codepoint.ReadAt(charactersSpan, characterIndex, out var characterLength);
characterIndex += characterLength;
if (!codepoint.IsWhiteSpace)
{
break;
@ -784,9 +783,9 @@ namespace Avalonia.Media
var j = i;
while (j - 1 >= 0)
while (j + 1 < _glyphInfos.Count)
{
var nextCluster = _glyphInfos[--j].GlyphCluster;
var nextCluster = _glyphInfos[++j].GlyphCluster;
if (currentCluster == nextCluster)
{
@ -798,6 +797,8 @@ namespace Avalonia.Media
break;
}
characterIndex -= clusterLength;
if (codepoint.IsBreakChar)
{
newLineLength += clusterLength;

37
src/Avalonia.Base/Media/MediaContext.Clock.cs

@ -4,6 +4,7 @@ using System.Diagnostics;
using Avalonia.Animation;
using Avalonia.Reactive;
using Avalonia.Threading;
using Avalonia.Utilities;
namespace Avalonia.Media;
@ -17,8 +18,12 @@ internal partial class MediaContext
{
private readonly MediaContext _parent;
private List<IObserver<TimeSpan>> _observers = new();
public bool HasNewSubscriptions { get; set; }
public bool HasSubscriptions => _observers.Count > 0;
private List<IObserver<TimeSpan>> _newObservers = new();
private Queue<Action<TimeSpan>> _queuedAnimationFrames = new();
private Queue<Action<TimeSpan>> _queuedAnimationFramesNext = new();
private TimeSpan _currentAnimationTimestamp;
public bool HasNewSubscriptions => _newObservers.Count > 0;
public bool HasSubscriptions => _observers.Count > 0 || _queuedAnimationFrames.Count > 0;
public MediaContextClock(MediaContext parent)
{
@ -29,19 +34,41 @@ internal partial class MediaContext
{
_parent.ScheduleRender(false);
Dispatcher.UIThread.VerifyAccess();
HasNewSubscriptions = true;
_observers.Add(observer);
_newObservers.Add(observer);
return Disposable.Create(() =>
{
Dispatcher.UIThread.VerifyAccess();
_observers.Remove(observer);
});
}
public void RequestAnimationFrame(Action<TimeSpan> action)
{
_parent.ScheduleRender(false);
_queuedAnimationFrames.Enqueue(action);
}
public void Pulse(TimeSpan now)
{
_newObservers.Clear();
_currentAnimationTimestamp = now;
// We are swapping the queues before enumeration
(_queuedAnimationFrames, _queuedAnimationFramesNext) = (_queuedAnimationFramesNext, _queuedAnimationFrames);
var animationFrames = _queuedAnimationFramesNext;
while (animationFrames.TryDequeue(out var callback))
callback(now);
foreach (var observer in _observers.ToArray())
observer.OnNext(now);
observer.OnNext(_currentAnimationTimestamp);
}
public void PulseNewSubscriptions()
{
foreach (var observer in _newObservers.ToArray())
observer.OnNext(_currentAnimationTimestamp);
_newObservers.Clear();
}
public PlayState PlayState
@ -50,4 +77,6 @@ internal partial class MediaContext
set => throw new InvalidOperationException();
}
}
public void RequestAnimationFrame(Action<TimeSpan> action) => _clock.RequestAnimationFrame(action);
}

3
src/Avalonia.Base/Media/MediaContext.Compositor.cs

@ -1,4 +1,5 @@
using System;
using System.Linq;
using System.Threading.Tasks;
using Avalonia.Platform;
using Avalonia.Rendering.Composition;
@ -78,7 +79,7 @@ partial class MediaContext
// Nothing to do, and there are no pending commits
return false;
foreach (var c in _requestedCommits)
foreach (var c in _requestedCommits.ToArray())
CommitCompositor(c);
_requestedCommits.Clear();

3
src/Avalonia.Base/Media/MediaContext.cs

@ -131,12 +131,11 @@ internal partial class MediaContext : ICompositorScheduler
// We are doing several iterations when it happens
for (var c = 0; c < 10; c++)
{
_clock.HasNewSubscriptions = false;
FireInvokeOnRenderCallbacks();
if (_clock.HasNewSubscriptions)
{
_clock.Pulse(now);
_clock.PulseNewSubscriptions();
continue;
}

2
src/Avalonia.Base/Media/TextFormatting/TextFormatterImpl.cs

@ -684,7 +684,9 @@ namespace Avalonia.Media.TextFormatting
var textRuns = new TextRun[] { new ShapedTextRun(shapedBuffer, properties) };
var line = new TextLineImpl(textRuns, firstTextSourceIndex, 0, paragraphWidth, paragraphProperties, flowDirection);
line.FinalizeLine();
return line;
}

22
src/Avalonia.Base/Media/TextFormatting/TextLayout.cs

@ -128,7 +128,7 @@ namespace Avalonia.Media.TextFormatting
/// <summary>
/// Gets the text spacing.
/// </summary>
public double LetterSpacing => _paragraphProperties.LetterSpacing;
public double LetterSpacing => _paragraphProperties.LetterSpacing;
/// <summary>
/// Gets the text lines.
@ -271,11 +271,13 @@ namespace Avalonia.Media.TextFormatting
var currentY = 0.0;
foreach (var textLine in _textLines)
for (var i = 0; i < _textLines.Length; i++)
{
var textLine = _textLines[i];
var end = textLine.FirstTextSourceIndex + textLine.Length;
if (end <= textPosition && end < _textSourceLength)
if (end <= textPosition && i + 1 < _textLines.Length)
{
currentY += textLine.Height;
@ -511,7 +513,7 @@ namespace Avalonia.Media.TextFormatting
{
var textLine = TextFormatterImpl.CreateEmptyTextLine(0, double.PositiveInfinity, _paragraphProperties);
UpdateMetrics(textLine, ref lineStartOfLongestLine, ref origin, ref first,
UpdateMetrics(textLine, ref lineStartOfLongestLine, ref origin, ref first,
ref accBlackBoxLeft, ref accBlackBoxTop, ref accBlackBoxRight, ref accBlackBoxBottom);
return new TextLine[] { textLine };
@ -638,13 +640,13 @@ namespace Avalonia.Media.TextFormatting
}
private void UpdateMetrics(
TextLine currentLine,
ref double lineStartOfLongestLine,
ref Point origin,
ref bool first,
TextLine currentLine,
ref double lineStartOfLongestLine,
ref Point origin,
ref bool first,
ref double accBlackBoxLeft,
ref double accBlackBoxTop,
ref double accBlackBoxRight,
ref double accBlackBoxTop,
ref double accBlackBoxRight,
ref double accBlackBoxBottom)
{
var blackBoxLeft = origin.X + currentLine.Start + currentLine.OverhangLeading;

131
src/Avalonia.Base/Media/TextFormatting/TextLineImpl.cs

@ -371,14 +371,16 @@ namespace Avalonia.Media.TextFormatting
IndexedTextRun currentIndexedRun = _indexedTextRuns[i];
while(currentIndexedRun.TextSourceCharacterIndex != currentPosition)
while (currentIndexedRun.TextSourceCharacterIndex != currentPosition)
{
if(i + 1 < _indexedTextRuns.Count)
if (i + 1 == _indexedTextRuns.Count)
{
i++;
currentIndexedRun = _indexedTextRuns[i];
break;
}
i++;
currentIndexedRun = _indexedTextRuns[i];
}
return currentIndexedRun;
@ -430,7 +432,7 @@ namespace Avalonia.Media.TextFormatting
if (currentTextRun == null)
{
return 0;
return Start;
}
var directionalWidth = 0.0;
@ -584,6 +586,8 @@ namespace Avalonia.Media.TextFormatting
var currentPosition = FirstTextSourceIndex;
var remainingLength = textLength;
TextBounds? lastBounds = null;
static FlowDirection GetDirection(TextRun textRun, FlowDirection currentDirection)
{
if (textRun is ShapedTextRun shapedTextRun)
@ -604,12 +608,14 @@ namespace Avalonia.Media.TextFormatting
while (currentIndexedRun.TextSourceCharacterIndex != currentPosition)
{
if (i + 1 < _indexedTextRuns.Count)
if (i + 1 == _indexedTextRuns.Count)
{
i++;
currentIndexedRun = _indexedTextRuns[i];
break;
}
i++;
currentIndexedRun = _indexedTextRuns[i];
}
return currentIndexedRun;
@ -632,6 +638,40 @@ namespace Avalonia.Media.TextFormatting
return distance;
}
bool TryMergeWithLastBounds(TextBounds currentBounds, TextBounds lastBounds)
{
if (currentBounds.FlowDirection != lastBounds.FlowDirection)
{
return false;
}
if (currentBounds.Rectangle.Left == lastBounds.Rectangle.Right)
{
foreach (var runBounds in currentBounds.TextRunBounds)
{
lastBounds.TextRunBounds.Add(runBounds);
}
lastBounds.Rectangle = lastBounds.Rectangle.Union(currentBounds.Rectangle);
return true;
}
if (currentBounds.Rectangle.Right == lastBounds.Rectangle.Left)
{
for (int i = 0; i < currentBounds.TextRunBounds.Count; i++)
{
lastBounds.TextRunBounds.Insert(i, currentBounds.TextRunBounds[i]);
}
lastBounds.Rectangle = lastBounds.Rectangle.Union(currentBounds.Rectangle);
return true;
}
return false;
}
while (remainingLength > 0 && currentPosition < FirstTextSourceIndex + Length)
{
var currentIndexedRun = FindIndexedRun();
@ -667,67 +707,21 @@ namespace Avalonia.Media.TextFormatting
directionalWidth = currentDrawable.Size.Width;
}
if (currentTextRun is not TextEndOfLine)
{
if (currentDirection == FlowDirection.LeftToRight)
{
// Find consecutive runs of same direction
for (; lastRunIndex + 1 < _textRuns.Length; lastRunIndex++)
{
var nextRun = _textRuns[lastRunIndex + 1];
var nextDirection = GetDirection(nextRun, currentDirection);
if (currentDirection != nextDirection)
{
break;
}
if (nextRun is DrawableTextRun nextDrawable)
{
directionalWidth += nextDrawable.Size.Width;
}
}
}
else
{
// Find consecutive runs of same direction
for (; firstRunIndex - 1 > 0; firstRunIndex--)
{
var previousRun = _textRuns[firstRunIndex - 1];
var previousDirection = GetDirection(previousRun, currentDirection);
if (currentDirection != previousDirection)
{
break;
}
if (previousRun is DrawableTextRun previousDrawable)
{
directionalWidth += previousDrawable.Size.Width;
currentX -= previousDrawable.Size.Width;
}
}
}
}
int coveredLength;
TextBounds? textBounds;
TextBounds? currentBounds;
switch (currentDirection)
{
case FlowDirection.RightToLeft:
{
textBounds = GetTextRunBoundsRightToLeft(firstRunIndex, lastRunIndex, currentX + directionalWidth, firstTextSourceIndex,
currentBounds = GetTextRunBoundsRightToLeft(firstRunIndex, lastRunIndex, currentX + directionalWidth, firstTextSourceIndex,
currentPosition, remainingLength, out coveredLength, out currentPosition);
break;
}
default:
{
textBounds = GetTextBoundsLeftToRight(firstRunIndex, lastRunIndex, currentX, firstTextSourceIndex,
currentBounds = GetTextBoundsLeftToRight(firstRunIndex, lastRunIndex, currentX, firstTextSourceIndex,
currentPosition, remainingLength, out coveredLength, out currentPosition);
break;
@ -736,7 +730,18 @@ namespace Avalonia.Media.TextFormatting
if (coveredLength > 0)
{
result.Add(textBounds);
if (lastBounds != null && TryMergeWithLastBounds(currentBounds, lastBounds))
{
currentBounds = lastBounds;
result[result.Count - 1] = currentBounds;
}
else
{
result.Add(currentBounds);
}
lastBounds = currentBounds;
remainingLength -= coveredLength;
}
@ -997,14 +1002,14 @@ namespace Avalonia.Media.TextFormatting
public void FinalizeLine()
{
_indexedTextRuns = BidiReorderer.Instance.BidiReorder(_textRuns, _paragraphProperties.FlowDirection, FirstTextSourceIndex);
_textLineMetrics = CreateLineMetrics();
if (_textLineBreak is null && _textRuns.Length > 1 && _textRuns[_textRuns.Length - 1] is TextEndOfLine textEndOfLine)
{
_textLineBreak = new TextLineBreak(textEndOfLine);
}
_indexedTextRuns = BidiReorderer.Instance.BidiReorder(_textRuns, _paragraphProperties.FlowDirection, FirstTextSourceIndex);
}
}
/// <summary>

2
src/Avalonia.Base/Media/TextFormatting/Unicode/BiDiAlgorithm.cs

@ -687,7 +687,7 @@ namespace Avalonia.Media.TextFormatting.Unicode
/// <remarks>
/// This method resolves the sos and eos values for the run
/// and adds the run to the list
/// /// </remarks>
/// </remarks>
/// <param name="start">The index of the start of the run (in x9 removed units)</param>
/// <param name="length">The length of the run (in x9 removed units)</param>
/// <param name="level">The level of the run</param>

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

@ -15,12 +15,6 @@ namespace Avalonia.Media
/// </summary>
public abstract class Transform : Animatable, IMutableTransform, ICompositionRenderResource<ITransform>, ICompositorSerializable
{
static Transform()
{
Animation.Animation.RegisterAnimator<TransformAnimator>(prop =>
typeof(ITransform).IsAssignableFrom(prop.OwnerType));
}
internal Transform()
{

2
src/Avalonia.Base/Platform/IPlatformRenderInterface.cs

@ -18,7 +18,7 @@ namespace Avalonia.Platform
/// Creates an ellipse geometry implementation.
/// </summary>
/// <param name="rect">The bounds of the ellipse.</param>
/// <returns>An ellipse geometry..</returns>
/// <returns>An ellipse geometry.</returns>
IGeometryImpl CreateEllipseGeometry(Rect rect);
/// <summary>

21
src/Avalonia.Base/Platform/Storage/FileIO/StorageProviderHelpers.cs

@ -10,16 +10,19 @@ internal static class StorageProviderHelpers
{
public static IStorageItem? TryCreateBclStorageItem(string path)
{
var directory = new DirectoryInfo(path);
if (directory.Exists)
if (!string.IsNullOrWhiteSpace(path))
{
return new BclStorageFolder(directory);
}
var file = new FileInfo(path);
if (file.Exists)
{
return new BclStorageFile(file);
var directory = new DirectoryInfo(path);
if (directory.Exists)
{
return new BclStorageFolder(directory);
}
var file = new FileInfo(path);
if (file.Exists)
{
return new BclStorageFile(file);
}
}
return null;

7
src/Avalonia.Base/Point.cs

@ -16,13 +16,6 @@ namespace Avalonia
#endif
readonly struct Point : IEquatable<Point>
{
static Point()
{
#if !BUILDTASK
Animation.Animation.RegisterAnimator<PointAnimator>(prop => typeof(Point).IsAssignableFrom(prop.PropertyType));
#endif
}
/// <summary>
/// The X position.
/// </summary>

5
src/Avalonia.Base/Rect.cs

@ -11,11 +11,6 @@ namespace Avalonia
/// </summary>
public readonly struct Rect : IEquatable<Rect>
{
static Rect()
{
Animation.Animation.RegisterAnimator<RectAnimator>(prop => typeof(Rect).IsAssignableFrom(prop.PropertyType));
}
/// <summary>
/// The X position.
/// </summary>

7
src/Avalonia.Base/RelativePoint.cs

@ -54,13 +54,6 @@ namespace Avalonia
private readonly RelativeUnit _unit;
static RelativePoint()
{
#if !BUILDTASK
Animation.Animation.RegisterAnimator<RelativePointAnimator>(prop => typeof(RelativePoint).IsAssignableFrom(prop.PropertyType));
#endif
}
/// <summary>
/// Initializes a new instance of the <see cref="RelativePoint"/> struct.
/// </summary>

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

@ -130,7 +130,7 @@ namespace Avalonia.Rendering.Composition
Dispatcher.UIThread.VerifyAccess();
using var noPump = NonPumpingLockHelper.Use();
_nextCommit ??= new();
var commit = _nextCommit ??= new();
(_invokeBeforeCommitRead, _invokeBeforeCommitWrite) = (_invokeBeforeCommitWrite, _invokeBeforeCommitRead);
while (_invokeBeforeCommitRead.Count > 0)
@ -188,7 +188,7 @@ namespace Avalonia.Rendering.Composition
}, TaskContinuationOptions.ExecuteSynchronously);
_nextCommit = null;
return _pendingBatch;
return commit;
}
}

5
src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionDrawListVisual.cs

@ -14,7 +14,7 @@ namespace Avalonia.Rendering.Composition.Server;
/// <summary>
/// Server-side counterpart of <see cref="CompositionDrawListVisual"/>
/// </summary>
internal class ServerCompositionDrawListVisual : ServerCompositionContainerVisual
internal class ServerCompositionDrawListVisual : ServerCompositionContainerVisual, IServerRenderResourceObserver
{
#if DEBUG
// This is needed for debugging purposes so we could see inspect the associated visual from debugger
@ -37,6 +37,7 @@ internal class ServerCompositionDrawListVisual : ServerCompositionContainerVisua
{
_renderCommands?.Dispose();
_renderCommands = reader.ReadObject<ServerCompositionRenderData?>();
_renderCommands?.AddObserver(this);
}
base.DeserializeChangesCore(reader, committedAt);
}
@ -50,6 +51,8 @@ internal class ServerCompositionDrawListVisual : ServerCompositionContainerVisua
base.RenderCore(canvas, currentTransformedClip);
}
public void DependencyQueuedInvalidate(IServerRenderResource sender) => ValuesInvalidated();
#if DEBUG
public override string ToString()
{

25
src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionDrawingSurface.cs

@ -49,22 +49,31 @@ internal class ServerCompositionDrawingSurface : ServerCompositionSurface, IDisp
public void UpdateWithAutomaticSync(CompositionImportedGpuImage image)
{
PerformSanityChecks(image);
Update(image.Image.SnapshotWithAutomaticSync(), image.Context);
using (Compositor.RenderInterface.EnsureCurrent())
{
PerformSanityChecks(image);
Update(image.Image.SnapshotWithAutomaticSync(), image.Context);
}
}
public void UpdateWithKeyedMutex(CompositionImportedGpuImage image, uint acquireIndex, uint releaseIndex)
{
PerformSanityChecks(image);
Update(image.Image.SnapshotWithKeyedMutex(acquireIndex, releaseIndex), image.Context);
using (Compositor.RenderInterface.EnsureCurrent())
{
PerformSanityChecks(image);
Update(image.Image.SnapshotWithKeyedMutex(acquireIndex, releaseIndex), image.Context);
}
}
public void UpdateWithSemaphores(CompositionImportedGpuImage image, CompositionImportedGpuSemaphore wait, CompositionImportedGpuSemaphore signal)
{
PerformSanityChecks(image);
if (!wait.IsUsable || !signal.IsUsable)
throw new PlatformGraphicsContextLostException();
Update(image.Image.SnapshotWithSemaphores(wait.Semaphore, signal.Semaphore), image.Context);
using (Compositor.RenderInterface.EnsureCurrent())
{
PerformSanityChecks(image);
if (!wait.IsUsable || !signal.IsUsable)
throw new PlatformGraphicsContextLostException();
Update(image.Image.SnapshotWithSemaphores(wait.Semaphore, signal.Semaphore), image.Context);
}
}
public void Dispose()

13
src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionTarget.cs

@ -128,7 +128,9 @@ namespace Avalonia.Rendering.Composition.Server
if (_renderTarget?.IsCorrupted == true)
{
_renderTarget!.Dispose();
_layer?.Dispose();
_layer = null;
_renderTarget.Dispose();
_renderTarget = null;
_redrawRequested = true;
}
@ -157,14 +159,15 @@ namespace Avalonia.Rendering.Composition.Server
_redrawRequested = false;
using (var targetContext = _renderTarget.CreateDrawingContext())
{
var layerSize = Size * Scaling;
var size = Size;
var layerSize = size * Scaling;
if (layerSize != _layerSize || _layer == null || _layer.IsCorrupted)
{
_layer?.Dispose();
_layer = null;
_layer = targetContext.CreateLayer(Size);
_layer = targetContext.CreateLayer(size);
_layerSize = layerSize;
_dirtyRect = new Rect(0, 0, layerSize.Width, layerSize.Height);
_dirtyRect = new Rect(0, 0, size.Width, size.Height);
}
if (_dirtyRect.Width != 0 || _dirtyRect.Height != 0)
@ -185,7 +188,7 @@ namespace Avalonia.Rendering.Composition.Server
else
targetContext.DrawBitmap(_layer, 1,
new Rect(_layerSize),
new Rect(Size));
new Rect(size));
if (DebugOverlays != RendererDebugOverlays.None)
{

10
src/Avalonia.Base/Rendering/Composition/Server/ServerRenderResource.cs

@ -13,8 +13,8 @@ internal interface IServerRenderResourceObserver
internal interface IServerRenderResource : IServerRenderResourceObserver
{
void AddObserver(IServerRenderResource observer);
void RemoveObserver(IServerRenderResource observer);
void AddObserver(IServerRenderResourceObserver observer);
void RemoveObserver(IServerRenderResourceObserver observer);
void QueuedInvalidate();
}
@ -23,7 +23,7 @@ internal class SimpleServerRenderResource : SimpleServerObject, IServerRenderRes
private bool _pendingInvalidation;
private bool _disposed;
public bool IsDisposed => _disposed;
private RefCountingSmallDictionary<IServerRenderResource> _observers;
private RefCountingSmallDictionary<IServerRenderResourceObserver> _observers;
public SimpleServerRenderResource(ServerCompositor compositor) : base(compositor)
{
@ -97,7 +97,7 @@ internal class SimpleServerRenderResource : SimpleServerObject, IServerRenderRes
}
public void AddObserver(IServerRenderResource observer)
public void AddObserver(IServerRenderResourceObserver observer)
{
Debug.Assert(!_disposed);
if(_disposed)
@ -105,7 +105,7 @@ internal class SimpleServerRenderResource : SimpleServerObject, IServerRenderRes
_observers.Add(observer);
}
public void RemoveObserver(IServerRenderResource observer)
public void RemoveObserver(IServerRenderResourceObserver observer)
{
if (_disposed)
return;

7
src/Avalonia.Base/Size.cs

@ -15,13 +15,6 @@ namespace Avalonia
#endif
readonly struct Size : IEquatable<Size>
{
static Size()
{
#if !BUILDTASK
Animation.Animation.RegisterAnimator<SizeAnimator>(prop => typeof(Size).IsAssignableFrom(prop.PropertyType));
#endif
}
/// <summary>
/// A size representing infinity.
/// </summary>

7
src/Avalonia.Base/Thickness.cs

@ -15,13 +15,6 @@ namespace Avalonia
#endif
readonly struct Thickness : IEquatable<Thickness>
{
static Thickness()
{
#if !BUILDTASK
Animation.Animation.RegisterAnimator<ThicknessAnimator>(prop => typeof(Thickness).IsAssignableFrom(prop.PropertyType));
#endif
}
/// <summary>
/// The thickness on the left.
/// </summary>

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

@ -306,7 +306,7 @@ namespace Avalonia.Utilities
/// if the value could not be converted.
/// </summary>
/// <param name="value">The value to convert.</param>
/// <param name="type">The type to convert to..</param>
/// <param name="type">The type to convert to.</param>
/// <param name="culture">The culture to use.</param>
/// <returns>A value of <paramref name="type"/>.</returns>
[RequiresUnreferencedCode(TrimmingMessages.TypeConversionRequiresUnreferencedCodeMessage)]

7
src/Avalonia.Base/Vector.cs

@ -17,13 +17,6 @@ namespace Avalonia
#endif
readonly struct Vector : IEquatable<Vector>
{
static Vector()
{
#if !BUILDTASK
Animation.Animation.RegisterAnimator<VectorAnimator>(prop => typeof(Vector).IsAssignableFrom(prop.PropertyType));
#endif
}
/// <summary>
/// The X component.
/// </summary>

40
src/Avalonia.Controls.ColorPicker/ColorPalettes/FlatColorPalette.cs

@ -266,26 +266,26 @@ namespace Avalonia.Controls
MidnightBlue9 = 0xFF1C2833,
MidnightBlue10 = 0xFF17202A,
Pomegranate = Pomegranate3,
Alizarin = Alizarin3,
Amethyst = Amethyst3,
Wisteria = Wisteria3,
BelizeHole = BelizeHole3,
PeterRiver = PeterRiver3,
Turquoise = Turquoise3,
GreenSea = GreenSea3,
Nephritis = Nephritis3,
Emerald = Emerald3,
Sunflower = Sunflower3,
Orange = Orange3,
Carrot = Carrot3,
Pumpkin = Pumpkin3,
Clouds = Clouds3,
Silver = Silver3,
Concrete = Concrete3,
Asbestos = Asbestos3,
WetAsphalt = WetAsphalt3,
MidnightBlue = MidnightBlue3,
Pomegranate = Pomegranate6,
Alizarin = Alizarin6,
Amethyst = Amethyst6,
Wisteria = Wisteria6,
BelizeHole = BelizeHole6,
PeterRiver = PeterRiver6,
Turquoise = Turquoise6,
GreenSea = GreenSea6,
Nephritis = Nephritis6,
Emerald = Emerald6,
Sunflower = Sunflower6,
Orange = Orange6,
Carrot = Carrot6,
Pumpkin = Pumpkin6,
Clouds = Clouds6,
Silver = Silver6,
Concrete = Concrete6,
Asbestos = Asbestos6,
WetAsphalt = WetAsphalt6,
MidnightBlue = MidnightBlue6,
};
// See: https://htmlcolorcodes.com/assets/downloads/flat-design-colors/flat-design-color-chart.png

18
src/Avalonia.Controls.ColorPicker/ColorSpectrum/ColorSpectrum.cs

@ -45,7 +45,6 @@ namespace Avalonia.Controls.Primitives
private bool _updatingColor = false;
private bool _updatingHsvColor = false;
private bool _coercedInitialColor = false;
private bool _isPointerPressed = false;
private bool _shouldShowLargeSelection = false;
private List<Hsv> _hsvValues = new List<Hsv>();
@ -622,7 +621,7 @@ namespace Avalonia.Controls.Primitives
// that no color has been selected by the user. Note that #00000000 is different than
// #00FFFFFF (Transparent).
//
// In this situation, the first time the user clicks on the spectrum the third
// In this situation, whenever the user clicks on the spectrum, the third
// component and alpha component will remain zero. This is because the spectrum only
// controls two components at any given time.
//
@ -633,16 +632,19 @@ namespace Avalonia.Controls.Primitives
// though the desired value is simply full color.
//
// To work around this usability issue with an initial #00000000 color, the selected
// color is coerced (only the first time) into a color with maximum third component
// value and maximum alpha. This can only happen once and only if those two components
// are already zero.
// color is coerced into a color with maximum third component value and maximum alpha.
// This can only happen here in the spectrum if those two components are already zero.
//
// In the past this coercion was restricted to occur only one time. However, when
// ColorPicker controls are re-used or recycled #00000000 can be set multiple times.
// Each time needs this special logic for usability so now anytime the color is
// changed on the spectrum this logic will run.
//
// Also note this is NOT currently done for #00FFFFFF (Transparent) but based on
// further usability study that case may need to be handled here as well. Right now
// Transparent is treated as a normal color value with the alpha intentionally set
// to zero so the alpha slider must still be adjusted after the spectrum.
if (!_coercedInitialColor &&
IsLoaded)
if (IsLoaded)
{
bool isAlphaComponentZero = (alpha == 0.0);
bool isThirdComponentZero = false;
@ -691,8 +693,6 @@ namespace Avalonia.Controls.Primitives
newHsv.H = 360.0;
break;
}
_coercedInitialColor = true;
}
}

10
src/Avalonia.Controls.DataGrid/DataGridColumn.cs

@ -1103,6 +1103,16 @@ namespace Avalonia.Controls
get;
set;
}
/// <summary>
/// Gets or sets an object associated with this column.
/// </summary>
public object Tag
{
get;
set;
}
/// <summary>
/// Holds a Comparer to use for sorting, if not using the default.
/// </summary>

19
src/Avalonia.Controls/Embedding/Offscreen/OffscreenTopLevelImpl.cs

@ -1,14 +1,10 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using Avalonia.Input;
using Avalonia.Input.Raw;
using Avalonia.Media;
using Avalonia.Metadata;
using Avalonia.Platform;
using Avalonia.Rendering;
using Avalonia.Rendering.Composition;
using Avalonia.Threading;
namespace Avalonia.Controls.Embedding.Offscreen
{
@ -17,7 +13,6 @@ namespace Avalonia.Controls.Embedding.Offscreen
{
private double _scaling = 1;
private Size _clientSize;
private ManualRenderTimer _manualRenderTimer = new();
public IInputRoot? InputRoot { get; private set; }
public bool IsDisposed { get; private set; }
@ -27,21 +22,10 @@ namespace Avalonia.Controls.Embedding.Offscreen
IsDisposed = true;
}
class ManualRenderTimer : IRenderTimer
{
static Stopwatch St = Stopwatch.StartNew();
public event Action<TimeSpan>? Tick;
public bool RunsInBackground => false;
public void TriggerTick() => Tick?.Invoke(St.Elapsed);
}
public Compositor Compositor { get; }
public OffscreenTopLevelImplBase()
{
Compositor = new Compositor(new RenderLoop(_manualRenderTimer), null, false,
MediaContext.Instance, false);
}
=> Compositor = new Compositor(null);
public abstract IEnumerable<object> Surfaces { get; }
@ -76,7 +60,6 @@ namespace Avalonia.Controls.Embedding.Offscreen
public void SetFrameThemeVariant(PlatformThemeVariant themeVariant) { }
/// <inheritdoc/>
public AcrylicPlatformCompensationLevels AcrylicCompensationLevels { get; } = new AcrylicPlatformCompensationLevels(1, 1, 1);
public void SetInputRoot(IInputRoot inputRoot) => InputRoot = inputRoot;

12
src/Avalonia.Controls/Platform/ManagedDispatcherImpl.cs

@ -99,9 +99,15 @@ public class ManagedDispatcherImpl : IControlledDispatcherImpl
continue;
}
if (_nextTimer != null)
TimeSpan? nextTimer;
lock (_lock)
{
nextTimer = _nextTimer;
}
if (nextTimer != null)
{
var waitFor = _clock.Elapsed - _nextTimer.Value;
var waitFor = nextTimer.Value - _clock.Elapsed;
if (waitFor.TotalMilliseconds < 1)
continue;
_wakeup.WaitOne(waitFor);
@ -112,4 +118,4 @@ public class ManagedDispatcherImpl : IControlledDispatcherImpl
registration.Dispose();
}
}
}

1
src/Avalonia.Controls/Primitives/OverlayLayer.cs

@ -6,6 +6,7 @@ namespace Avalonia.Controls.Primitives
{
public class OverlayLayer : Canvas
{
protected override bool BypassFlowDirectionPolicies => true;
public Size AvailableSize { get; private set; }
public static OverlayLayer? GetOverlayLayer(Visual visual)
{

2
src/Avalonia.Controls/Primitives/PopupPositioning/IPopupPositioner.cs

@ -216,7 +216,7 @@ namespace Avalonia.Controls.Primitives.PopupPositioning
///
/// If the adjusted position also ends up being constrained, the resulting position of the
/// FlipX adjustment will be the one before the adjustment.
/// /// </remarks>
/// </remarks>
FlipX = 4,
/// <summary>

116
src/Avalonia.Controls/Remote/Server/RemoteServerTopLevelImpl.Framebuffer.cs

@ -0,0 +1,116 @@
using System;
using System.Runtime.InteropServices;
using System.Threading;
using Avalonia.Platform;
using Avalonia.Remote.Protocol.Viewport;
using PlatformPixelFormat = Avalonia.Platform.PixelFormat;
using ProtocolPixelFormat = Avalonia.Remote.Protocol.Viewport.PixelFormat;
namespace Avalonia.Controls.Remote.Server
{
internal partial class RemoteServerTopLevelImpl
{
private enum FrameStatus
{
NotRendered,
Rendered,
CopiedToMessage
}
private sealed class Framebuffer
{
public static Framebuffer Empty { get; } = new(ProtocolPixelFormat.Rgba8888, default, 1.0);
private readonly double _dpi;
private readonly PixelSize _frameSize;
private readonly object _dataLock = new();
private readonly byte[] _data; // for rendering only
private readonly byte[] _dataCopy; // for messages only
private FrameStatus _status = FrameStatus.NotRendered;
public Framebuffer(ProtocolPixelFormat format, Size clientSize, double renderScaling)
{
var frameSize = PixelSize.FromSize(clientSize, renderScaling);
if (frameSize.Width <= 0 || frameSize.Height <= 0)
frameSize = PixelSize.Empty;
var bpp = format == ProtocolPixelFormat.Rgb565 ? 2 : 4;
var stride = frameSize.Width * bpp;
var dataLength = Math.Max(0, stride * frameSize.Height);
_dpi = renderScaling * 96.0;
_frameSize = frameSize;
Format = format;
ClientSize = clientSize;
RenderScaling = renderScaling;
(Stride, _data, _dataCopy) = dataLength > 0 ?
(stride, new byte[dataLength], new byte[dataLength]) :
(0, Array.Empty<byte>(), Array.Empty<byte>());
}
public ProtocolPixelFormat Format { get; }
public Size ClientSize { get; }
public double RenderScaling { get; }
public int Stride { get; }
public FrameStatus GetStatus()
{
lock (_dataLock)
return _status;
}
public ILockedFramebuffer Lock(Action onUnlocked)
{
var handle = GCHandle.Alloc(_data, GCHandleType.Pinned);
Monitor.Enter(_dataLock);
try
{
return new LockedFramebuffer(
handle.AddrOfPinnedObject(),
_frameSize,
Stride,
new Vector(_dpi, _dpi),
new PlatformPixelFormat((PixelFormatEnum)Format),
() =>
{
handle.Free();
Array.Copy(_data, _dataCopy, _data.Length);
_status = FrameStatus.Rendered;
Monitor.Exit(_dataLock);
onUnlocked();
});
}
catch
{
handle.Free();
Monitor.Exit(_dataLock);
throw;
}
}
/// <remarks>The returned message must NOT be kept around, as it contains a shared buffer.</remarks>
public FrameMessage ToMessage(long sequenceId)
{
lock (_dataLock)
_status = FrameStatus.CopiedToMessage;
return new FrameMessage
{
SequenceId = sequenceId,
Data = _dataCopy,
Format = Format,
Width = _frameSize.Width,
Height = _frameSize.Height,
Stride = Stride,
DpiX = _dpi,
DpiY = _dpi
};
}
}
}
}

365
src/Avalonia.Controls/Remote/Server/RemoteServerTopLevelImpl.cs

@ -1,6 +1,5 @@
using System;
using System.Collections.Generic;
using System.Runtime.InteropServices;
using Avalonia.Controls.Embedding.Offscreen;
using Avalonia.Controls.Platform.Surfaces;
using Avalonia.Input;
@ -11,7 +10,6 @@ using Avalonia.Platform;
using Avalonia.Remote.Protocol;
using Avalonia.Remote.Protocol.Input;
using Avalonia.Remote.Protocol.Viewport;
using Avalonia.Rendering;
using Avalonia.Threading;
using Key = Avalonia.Input.Key;
using ProtocolPixelFormat = Avalonia.Remote.Protocol.Viewport.PixelFormat;
@ -20,28 +18,28 @@ using ProtocolMouseButton = Avalonia.Remote.Protocol.Input.MouseButton;
namespace Avalonia.Controls.Remote.Server
{
[Unstable]
internal class RemoteServerTopLevelImpl : OffscreenTopLevelImplBase, IFramebufferPlatformSurface, ITopLevelImpl
internal partial class RemoteServerTopLevelImpl : OffscreenTopLevelImplBase, IFramebufferPlatformSurface, ITopLevelImpl
{
private readonly IAvaloniaRemoteTransportConnection _transport;
private LockedFramebuffer? _framebuffer;
private readonly object _lock = new();
private readonly Action _sendLastFrameIfNeeded;
private readonly Action _renderAndSendFrameIfNeeded;
private Framebuffer _framebuffer = Framebuffer.Empty;
private long _lastSentFrame = -1;
private long _lastReceivedFrame = -1;
private long _nextFrameNumber = 1;
private ClientViewportAllocatedMessage? _pendingAllocation;
private bool _queuedNextRender;
private bool _inRender;
private Vector _dpi = new Vector(96, 96);
private ProtocolPixelFormat[]? _supportedFormats;
private ProtocolPixelFormat? _format;
public RemoteServerTopLevelImpl(IAvaloniaRemoteTransportConnection transport)
{
_sendLastFrameIfNeeded = SendLastFrameIfNeeded;
_renderAndSendFrameIfNeeded = RenderAndSendFrameIfNeeded;
_transport = transport;
_transport.OnMessage += OnMessage;
KeyboardDevice = AvaloniaLocator.Current.GetRequiredService<IKeyboardDevice>();
QueueNextRender();
Compositor.AfterCommit += QueueNextRender;
}
private static RawPointerEventType GetAvaloniaEventType(ProtocolMouseButton button, bool pressed)
@ -112,45 +110,41 @@ namespace Avalonia.Controls.Remote.Server
{
lock (_lock)
{
if (obj is FrameReceivedMessage lastFrame)
switch (obj)
{
lock (_lock)
{
case FrameReceivedMessage lastFrame:
_lastReceivedFrame = Math.Max(lastFrame.SequenceId, _lastReceivedFrame);
}
Dispatcher.UIThread.Post(RenderIfNeeded);
}
if(obj is ClientRenderInfoMessage renderInfo)
{
lock(_lock)
{
_dpi = new Vector(renderInfo.DpiX, renderInfo.DpiY);
_queuedNextRender = true;
}
Dispatcher.UIThread.Post(RenderIfNeeded);
}
if (obj is ClientSupportedPixelFormatsMessage supportedFormats)
{
lock (_lock)
_supportedFormats = supportedFormats.Formats;
Dispatcher.UIThread.Post(RenderIfNeeded);
}
if (obj is MeasureViewportMessage measure)
Dispatcher.UIThread.Post(() =>
{
var m = Measure(new Size(measure.Width, measure.Height));
_transport.Send(new MeasureViewportMessage
Dispatcher.UIThread.Post(_sendLastFrameIfNeeded);
break;
case ClientRenderInfoMessage renderInfo:
Dispatcher.UIThread.Post(() =>
{
Width = m.Width,
Height = m.Height
RenderScaling = renderInfo.DpiX / 96.0;
RenderAndSendFrameIfNeeded();
});
});
if (obj is ClientViewportAllocatedMessage allocated)
{
lock (_lock)
{
break;
case ClientSupportedPixelFormatsMessage supportedFormats:
_format = TryGetValidPixelFormat(supportedFormats.Formats);
Dispatcher.UIThread.Post(_renderAndSendFrameIfNeeded);
break;
case MeasureViewportMessage measure:
Dispatcher.UIThread.Post(() =>
{
var m = Measure(new Size(measure.Width, measure.Height));
_transport.Send(new MeasureViewportMessage
{
Width = m.Width,
Height = m.Height
});
});
break;
case ClientViewportAllocatedMessage allocated:
if (_pendingAllocation == null)
{
Dispatcher.UIThread.Post(() =>
{
ClientViewportAllocatedMessage allocation;
@ -159,101 +153,111 @@ namespace Avalonia.Controls.Remote.Server
allocation = _pendingAllocation!;
_pendingAllocation = null;
}
_dpi = new Vector(allocation.DpiX, allocation.DpiY);
RenderScaling = allocation.DpiX / 96.0;
ClientSize = new Size(allocation.Width, allocation.Height);
RenderIfNeeded();
RenderAndSendFrameIfNeeded();
});
}
_pendingAllocation = allocated;
}
}
if(obj is PointerMovedEventMessage pointer)
{
Dispatcher.UIThread.Post(() =>
{
Input?.Invoke(new RawPointerEventArgs(
MouseDevice,
0,
InputRoot!,
RawPointerEventType.Move,
new Point(pointer.X, pointer.Y),
GetAvaloniaRawInputModifiers(pointer.Modifiers)));
}, DispatcherPriority.Input);
}
if(obj is PointerPressedEventMessage pressed)
{
Dispatcher.UIThread.Post(() =>
{
Input?.Invoke(new RawPointerEventArgs(
MouseDevice,
0,
InputRoot!,
GetAvaloniaEventType(pressed.Button, true),
new Point(pressed.X, pressed.Y),
GetAvaloniaRawInputModifiers(pressed.Modifiers)));
}, DispatcherPriority.Input);
}
if (obj is PointerReleasedEventMessage released)
{
Dispatcher.UIThread.Post(() =>
{
Input?.Invoke(new RawPointerEventArgs(
MouseDevice,
0,
InputRoot!,
GetAvaloniaEventType(released.Button, false),
new Point(released.X, released.Y),
GetAvaloniaRawInputModifiers(released.Modifiers)));
}, DispatcherPriority.Input);
}
if(obj is ScrollEventMessage scroll)
{
Dispatcher.UIThread.Post(() =>
{
Input?.Invoke(new RawMouseWheelEventArgs(
MouseDevice,
0,
InputRoot!,
new Point(scroll.X, scroll.Y),
new Vector(scroll.DeltaX, scroll.DeltaY),
GetAvaloniaRawInputModifiers(scroll.Modifiers)));
}, DispatcherPriority.Input);
}
if(obj is KeyEventMessage key)
{
Dispatcher.UIThread.Post(() =>
{
Dispatcher.UIThread.RunJobs(DispatcherPriority.Input + 1);
Input?.Invoke(new RawKeyEventArgs(
KeyboardDevice,
0,
InputRoot!,
key.IsDown ? RawKeyEventType.KeyDown : RawKeyEventType.KeyUp,
(Key)key.Key,
GetAvaloniaRawInputModifiers(key.Modifiers)));
}, DispatcherPriority.Input);
}
if(obj is TextInputEventMessage text)
{
Dispatcher.UIThread.Post(() =>
{
Dispatcher.UIThread.RunJobs(DispatcherPriority.Input + 1);
Input?.Invoke(new RawTextInputEventArgs(
KeyboardDevice,
0,
InputRoot!,
text.Text));
}, DispatcherPriority.Input);
break;
case PointerMovedEventMessage pointer:
Dispatcher.UIThread.Post(() =>
{
Input?.Invoke(new RawPointerEventArgs(
MouseDevice,
0,
InputRoot!,
RawPointerEventType.Move,
new Point(pointer.X, pointer.Y),
GetAvaloniaRawInputModifiers(pointer.Modifiers)));
}, DispatcherPriority.Input);
break;
case PointerPressedEventMessage pressed:
Dispatcher.UIThread.Post(() =>
{
Input?.Invoke(new RawPointerEventArgs(
MouseDevice,
0,
InputRoot!,
GetAvaloniaEventType(pressed.Button, true),
new Point(pressed.X, pressed.Y),
GetAvaloniaRawInputModifiers(pressed.Modifiers)));
}, DispatcherPriority.Input);
break;
case PointerReleasedEventMessage released:
Dispatcher.UIThread.Post(() =>
{
Input?.Invoke(new RawPointerEventArgs(
MouseDevice,
0,
InputRoot!,
GetAvaloniaEventType(released.Button, false),
new Point(released.X, released.Y),
GetAvaloniaRawInputModifiers(released.Modifiers)));
}, DispatcherPriority.Input);
break;
case ScrollEventMessage scroll:
Dispatcher.UIThread.Post(() =>
{
Input?.Invoke(new RawMouseWheelEventArgs(
MouseDevice,
0,
InputRoot!,
new Point(scroll.X, scroll.Y),
new Vector(scroll.DeltaX, scroll.DeltaY),
GetAvaloniaRawInputModifiers(scroll.Modifiers)));
}, DispatcherPriority.Input);
break;
case KeyEventMessage key:
Dispatcher.UIThread.Post(() =>
{
Dispatcher.UIThread.RunJobs(DispatcherPriority.Input + 1);
Input?.Invoke(new RawKeyEventArgs(
KeyboardDevice,
0,
InputRoot!,
key.IsDown ? RawKeyEventType.KeyDown : RawKeyEventType.KeyUp,
(Key)key.Key,
GetAvaloniaRawInputModifiers(key.Modifiers)));
}, DispatcherPriority.Input);
break;
case TextInputEventMessage text:
Dispatcher.UIThread.Post(() =>
{
Dispatcher.UIThread.RunJobs(DispatcherPriority.Input + 1);
Input?.Invoke(new RawTextInputEventArgs(
KeyboardDevice,
0,
InputRoot!,
text.Text));
}, DispatcherPriority.Input);
break;
}
}
}
protected void SetDpi(Vector dpi)
private static ProtocolPixelFormat? TryGetValidPixelFormat(ProtocolPixelFormat[]? formats)
{
_dpi = dpi;
RenderIfNeeded();
if (formats is not null)
{
foreach (var format in formats)
{
if (format is >= 0 and <= ProtocolPixelFormat.MaxValue)
return format;
}
}
return null;
}
protected virtual Size Measure(Size constraint)
@ -265,88 +269,63 @@ namespace Avalonia.Controls.Remote.Server
public override IEnumerable<object> Surfaces => new[] { this };
private FrameMessage RenderFrame(int width, int height, ProtocolPixelFormat? format)
private Framebuffer GetOrCreateFramebuffer()
{
var scalingX = _dpi.X / 96.0;
var scalingY = _dpi.Y / 96.0;
width = (int)(width * scalingX);
height = (int)(height * scalingY);
var fmt = format ?? ProtocolPixelFormat.Rgba8888;
var bpp = fmt == ProtocolPixelFormat.Rgb565 ? 2 : 4;
var data = new byte[width * height * bpp];
var handle = GCHandle.Alloc(data, GCHandleType.Pinned);
try
{
if (width > 0 && height > 0)
{
_framebuffer = new LockedFramebuffer(handle.AddrOfPinnedObject(), new PixelSize(width, height), width * bpp, _dpi, new((PixelFormatEnum)fmt),
null);
Paint?.Invoke(new Rect(0, 0, width, height));
}
}
finally
lock (_lock)
{
_framebuffer = null;
handle.Free();
if (_format is not { } format)
_framebuffer = Framebuffer.Empty;
else if (_framebuffer.Format != format || _framebuffer.ClientSize != ClientSize || _framebuffer.RenderScaling != RenderScaling)
_framebuffer = new Framebuffer(format, ClientSize, RenderScaling);
return _framebuffer;
}
return new FrameMessage
{
Data = data,
Format = fmt,
Width = width,
Height = height,
Stride = width * bpp,
DpiX = _dpi.X,
DpiY = _dpi.Y
};
}
public ILockedFramebuffer Lock()
{
if (_framebuffer == null)
throw new InvalidOperationException("Paint was not requested, wait for Paint event");
return _framebuffer;
}
=> GetOrCreateFramebuffer().Lock(_sendLastFrameIfNeeded);
protected void RenderIfNeeded()
private void SendLastFrameIfNeeded()
{
lock (_lock)
{
if (_lastReceivedFrame != _lastSentFrame || !_queuedNextRender || _supportedFormats == null)
return;
if (IsDisposed)
return;
}
Framebuffer framebuffer;
long sequenceId;
var format = ProtocolPixelFormat.Rgba8888;
foreach(var fmt in _supportedFormats)
if (fmt <= ProtocolPixelFormat.MaxValue)
{
format = fmt;
break;
}
_inRender = true;
var frame = RenderFrame((int) ClientSize.Width, (int) ClientSize.Height, format);
lock (_lock)
{
// Ideally we should only send a frame if its status is Rendered: since the renderer might not be
// initialized at the start, we're sending black frames in this case. However, this was the historical
// behavior and some external programs are depending on receiving a frame asap.
if (_lastReceivedFrame != _lastSentFrame || _framebuffer.GetStatus() == FrameStatus.CopiedToMessage)
return;
framebuffer = _framebuffer;
_lastSentFrame = _nextFrameNumber++;
frame.SequenceId = _lastSentFrame;
_queuedNextRender = false;
sequenceId = _lastSentFrame;
}
_inRender = false;
_transport.Send(frame);
_transport.Send(framebuffer.ToMessage(sequenceId));
}
private void QueueNextRender()
protected void RenderAndSendFrameIfNeeded()
{
if (!_inRender && !IsDisposed)
if (IsDisposed)
return;
lock (_lock)
{
_queuedNextRender = true;
DispatcherTimer.RunOnce(RenderIfNeeded, TimeSpan.FromMilliseconds(2), DispatcherPriority.Background);
if (_lastReceivedFrame != _lastSentFrame || _format is null)
return;
}
var framebuffer = GetOrCreateFramebuffer();
if (framebuffer.Stride > 0)
Paint?.Invoke(new Rect(framebuffer.ClientSize));
SendLastFrameIfNeeded();
}
public override IMouseDevice MouseDevice { get; } = new MouseDevice();

12
src/Avalonia.Controls/TextBlock.cs

@ -668,17 +668,7 @@ namespace Avalonia.Controls
if (HasComplexContent)
{
if (_textRuns != null)
{
foreach (var textRun in _textRuns)
{
if (textRun is EmbeddedControlRun controlRun &&
controlRun.Control is Control control)
{
VisualChildren.Remove(control);
}
}
}
VisualChildren.Clear();
var textRuns = new List<TextRun>();

47
src/Avalonia.Controls/ToggleSwitch.cs

@ -1,4 +1,5 @@
using Avalonia.Controls.Metadata;
using Avalonia.Animation;
using Avalonia.Controls.Metadata;
using Avalonia.Controls.Presenters;
using Avalonia.Controls.Primitives;
using Avalonia.Controls.Templates;
@ -42,6 +43,10 @@ namespace Avalonia.Controls
x.UpdateKnobPos(x.IsChecked.Value);
}
});
KnobTransitionsProperty.Changed.AddClassHandler<ToggleSwitch>((x, e) =>
{
x.UpdateKnobTransitions();
});
}
/// <summary>
@ -68,6 +73,12 @@ namespace Avalonia.Controls
public static readonly StyledProperty<IDataTemplate?> OnContentTemplateProperty =
AvaloniaProperty.Register<ToggleSwitch, IDataTemplate?>(nameof(OnContentTemplate));
/// <summary>
/// Defines the <see cref="KnobTransitions"/> property.
/// </summary>
public static readonly StyledProperty<Transitions> KnobTransitionsProperty =
AvaloniaProperty.Register<ToggleSwitch, Transitions>(nameof(KnobTransitions));
/// <summary>
/// Gets or Sets the Content that is displayed when in the On State.
/// </summary>
@ -116,6 +127,17 @@ namespace Avalonia.Controls
set { SetValue(OnContentTemplateProperty, value); }
}
/// <summary>
/// Gets or Sets the <see cref="Transitions"/> of switching knob.
/// </summary>
public Transitions KnobTransitions
{
get { return GetValue(KnobTransitionsProperty); }
set { SetValue(KnobTransitionsProperty, value); }
}
private void OffContentChanged(AvaloniaPropertyChangedEventArgs e)
{
if (e.OldValue is ILogical oldChild)
@ -177,7 +199,21 @@ namespace Avalonia.Controls
UpdateKnobPos(IsChecked.Value);
}
}
protected override void OnLoaded()
{
base.OnLoaded();
UpdateKnobTransitions();
}
private void UpdateKnobTransitions()
{
if (_knobsPanel != null)
{
_knobsPanel.Transitions = KnobTransitions;
}
}
private void KnobsPanel_PointerPressed(object? sender, Input.PointerPressedEventArgs e)
{
_switchStartPoint = e.GetPosition(_switchKnob);
@ -194,7 +230,7 @@ namespace Avalonia.Controls
_knobsPanel!.ClearValue(Canvas.LeftProperty);
PseudoClasses.Set(":dragging", false);
if (shouldBecomeChecked == IsChecked)
{
UpdateKnobPos(shouldBecomeChecked);
@ -203,6 +239,7 @@ namespace Avalonia.Controls
{
SetCurrentValue(IsCheckedProperty, shouldBecomeChecked);
}
UpdateKnobTransitions();
}
else
{
@ -218,6 +255,10 @@ namespace Avalonia.Controls
{
if (_knobsPanelPressed)
{
if(_knobsPanel != null)
{
_knobsPanel.Transitions = null;
}
var difference = e.GetPosition(_switchKnob) - _switchStartPoint;
if ((!_isDragging) && (System.Math.Abs(difference.X) > 3))

12
src/Avalonia.Controls/TopLevel.cs

@ -25,6 +25,7 @@ using System.Linq;
using System.Threading.Tasks;
using Avalonia.Metadata;
using Avalonia.Rendering.Composition;
using Avalonia.Threading;
namespace Avalonia.Controls
{
@ -535,7 +536,16 @@ namespace Avalonia.Controls
return Disposable.Create(() => { });
}
}
/// <summary>
/// Enqueues a callback to be called on the next animation tick
/// </summary>
public void RequestAnimationFrame(Action<TimeSpan> action)
{
Dispatcher.UIThread.VerifyAccess();
MediaContext.Instance.RequestAnimationFrame(action);
}
protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change)
{
base.OnPropertyChanged(change);

19
src/Avalonia.Controls/TreeView.cs

@ -84,6 +84,11 @@ namespace Avalonia.Controls
/// <summary>
/// Gets or sets a value indicating whether to automatically scroll to newly selected items.
/// </summary>
/// <remarks>
/// This property is of limited use with <see cref="TreeView"/> as it will only scroll
/// to realized items. To scroll to a non-expanded item, you need to ensure that its
/// ancestors are expanded.
/// </remarks>
public bool AutoScrollToSelectedItem
{
get => GetValue(AutoScrollToSelectedItemProperty);
@ -353,9 +358,13 @@ namespace Avalonia.Controls
SelectedItemsAdded(e.NewItems!.Cast<object>().ToArray());
if (AutoScrollToSelectedItem)
var selectedItem = SelectedItem;
if (AutoScrollToSelectedItem &&
selectedItem is not null &&
e.NewItems![0] == selectedItem)
{
var container = ContainerFromItem(e.NewItems![0]!);
var container = TreeContainerFromItem(selectedItem);
container?.BringIntoView();
}
@ -531,6 +540,12 @@ namespace Avalonia.Controls
// The IsSelected property is not set on the container: update the container
// selection based on the current selection as understood by this control.
MarkContainerSelected(container, SelectedItems.Contains(item));
// If the newly realized container is the selected container, scroll to it after layout.
if (AutoScrollToSelectedItem && SelectedItem == item)
{
Dispatcher.UIThread.Post(container.BringIntoView, DispatcherPriority.Loaded);
}
}
/// <inheritdoc/>

7
src/Avalonia.DesignerSupport/DesignWindowLoader.cs

@ -4,6 +4,7 @@ using System.Linq;
using System.Reflection;
using System.Text;
using Avalonia.Controls;
using Avalonia.Controls.Embedding.Offscreen;
using Avalonia.Controls.Platform;
using Avalonia.Markup.Xaml;
using Avalonia.Styling;
@ -13,6 +14,9 @@ namespace Avalonia.DesignerSupport
public class DesignWindowLoader
{
public static Window LoadDesignerWindow(string xaml, string assemblyPath, string xamlFileProjectPath)
=> LoadDesignerWindow(xaml, assemblyPath, xamlFileProjectPath, 1.0);
public static Window LoadDesignerWindow(string xaml, string assemblyPath, string xamlFileProjectPath, double renderScaling)
{
Window window;
Control control;
@ -96,6 +100,9 @@ namespace Avalonia.DesignerSupport
window = new Window() {Content = (Control)control};
}
if (window.PlatformImpl is OffscreenTopLevelImplBase offscreenImpl)
offscreenImpl.RenderScaling = renderScaling;
Design.ApplyDesignModeProperties(window, control);
if (!window.IsSet(Window.SizeToContentProperty))

12
src/Avalonia.DesignerSupport/Remote/PreviewerWindowImpl.cs

@ -53,7 +53,11 @@ namespace Avalonia.DesignerSupport.Remote
// In previewer mode we completely ignore client-side viewport size
if (obj is ClientViewportAllocatedMessage alloc)
{
Dispatcher.UIThread.Post(() => SetDpi(new Vector(alloc.DpiX, alloc.DpiY)));
Dispatcher.UIThread.Post(() =>
{
RenderScaling = alloc.DpiX / 96.0;
RenderAndSendFrameIfNeeded();
});
return;
}
base.OnMessage(transport, obj);
@ -63,11 +67,11 @@ namespace Avalonia.DesignerSupport.Remote
{
_transport.Send(new RequestViewportResizeMessage
{
Width = clientSize.Width,
Height = clientSize.Height
Width = Math.Ceiling(clientSize.Width * RenderScaling),
Height = Math.Ceiling(clientSize.Height * RenderScaling)
});
ClientSize = clientSize;
RenderIfNeeded();
RenderAndSendFrameIfNeeded();
}
public void Move(PixelPoint point)

2
src/Avalonia.DesignerSupport/Remote/PreviewerWindowingPlatform.cs

@ -52,7 +52,7 @@ namespace Avalonia.DesignerSupport.Remote
.Bind<IKeyboardDevice>().ToConstant(Keyboard)
.Bind<IPlatformSettings>().ToSingleton<DefaultPlatformSettings>()
.Bind<IDispatcherImpl>().ToConstant(new ManagedDispatcherImpl(null))
.Bind<IRenderTimer>().ToConstant(new DefaultRenderTimer(60))
.Bind<IRenderTimer>().ToConstant(new UiThreadRenderTimer(60))
.Bind<IWindowingPlatform>().ToConstant(instance)
.Bind<IPlatformIconLoader>().ToSingleton<IconLoaderStub>()
.Bind<PlatformHotkeyConfiguration>().ToSingleton<PlatformHotkeyConfiguration>();

9
src/Avalonia.DesignerSupport/Remote/RemoteDesignerEntryPoint.cs

@ -1,13 +1,10 @@
using System;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Net;
using System.Reflection;
using System.Threading;
using System.Xml;
using Avalonia.Controls;
using Avalonia.DesignerSupport.Remote.HtmlTransport;
using Avalonia.Input;
using Avalonia.Remote.Protocol;
using Avalonia.Remote.Protocol.Designer;
using Avalonia.Remote.Protocol.Viewport;
@ -20,6 +17,7 @@ namespace Avalonia.DesignerSupport.Remote
private static ClientSupportedPixelFormatsMessage s_supportedPixelFormats;
private static ClientViewportAllocatedMessage s_viewportAllocatedMessage;
private static ClientRenderInfoMessage s_renderInfoMessage;
private static double s_lastRenderScaling = 1.0;
private static IAvaloniaRemoteTransportConnection s_transport;
class CommandLineArgs
@ -226,6 +224,9 @@ namespace Avalonia.DesignerSupport.Remote
}
if (obj is UpdateXamlMessage xaml)
{
if (s_currentWindow is not null)
s_lastRenderScaling = s_currentWindow.RenderScaling;
try
{
s_currentWindow?.Close();
@ -237,7 +238,7 @@ namespace Avalonia.DesignerSupport.Remote
s_currentWindow = null;
try
{
s_currentWindow = DesignWindowLoader.LoadDesignerWindow(xaml.Xaml, xaml.AssemblyPath, xaml.XamlFileProjectPath);
s_currentWindow = DesignWindowLoader.LoadDesignerWindow(xaml.Xaml, xaml.AssemblyPath, xaml.XamlFileProjectPath, s_lastRenderScaling);
s_transport.Send(new UpdateXamlResultMessage(){Handle = s_currentWindow.PlatformImpl?.Handle?.Handle.ToString()});
}
catch (Exception e)

21
src/Avalonia.Diagnostics/Diagnostics/Controls/BrushEditor.axaml

@ -0,0 +1,21 @@
<Styles xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:controls="using:Avalonia.Diagnostics.Controls"
x:ClassModifier="internal">
<Design.PreviewWith>
<controls:BrushEditor />
</Design.PreviewWith>
<Style Selector=":is(controls|BrushEditor)">
<Setter Property="Template">
<ControlTemplate>
<Grid>
<Button Theme="{StaticResource SimpleTextBoxClearButtonTheme}"
x:Name="PART_ClearButton"
HorizontalAlignment="Right"
VerticalContentAlignment="Center"/>
</Grid>
</ControlTemplate>
</Setter>
</Style>
</Styles>

121
src/Avalonia.Diagnostics/Diagnostics/Controls/BrushEditor.axaml.cs

@ -0,0 +1,121 @@
using System;
using System.Globalization;
using Avalonia.Controls;
using Avalonia.Controls.Metadata;
using Avalonia.Controls.Primitives;
using Avalonia.Input;
using Avalonia.Interactivity;
using Avalonia.Media;
using Avalonia.Media.Immutable;
namespace Avalonia.Diagnostics.Controls
{
[TemplatePart("PART_ClearButton", typeof(Button))]
partial class BrushEditor : TemplatedControl
{
private readonly EventHandler<RoutedEventArgs> clearHandler;
private Button? _clearButton = default;
private readonly ColorView _colorView = new()
{
HexInputAlphaPosition = AlphaComponentPosition.Leading, // Always match XAML
};
public BrushEditor()
{
FlyoutBase.SetAttachedFlyout(this, new Flyout { Content = _colorView });
_colorView.ColorChanged += (_, e) => Brush = new ImmutableSolidColorBrush(e.NewColor);
clearHandler = (s, e) => Brush = default;
}
protected override Type StyleKeyOverride => typeof(BrushEditor);
protected override void OnApplyTemplate(TemplateAppliedEventArgs e)
{
base.OnApplyTemplate(e);
if (_clearButton is not null)
{
_clearButton.Click -= clearHandler;
}
_clearButton = e.NameScope.Find<Button>("PART_ClearButton");
if (_clearButton is Button button)
{
button.Click += clearHandler;
}
}
/// <summary>
/// Defines the <see cref="Brush" /> property.
/// </summary>
public static readonly DirectProperty<BrushEditor, IBrush?> BrushProperty =
AvaloniaProperty.RegisterDirect<BrushEditor, IBrush?>(
nameof(Brush), o => o.Brush, (o, v) => o.Brush = v);
private IBrush? _brush;
public IBrush? Brush
{
get => _brush;
set => SetAndRaise(BrushProperty, ref _brush, value);
}
protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change)
{
base.OnPropertyChanged(change);
if (change.Property == BrushProperty)
{
if (Brush is ISolidColorBrush scb)
{
_colorView.Color = scb.Color;
}
ToolTip.SetTip(this, Brush?.GetType().Name ?? "(null)");
InvalidateVisual();
}
}
protected override void OnPointerPressed(PointerPressedEventArgs e)
{
base.OnPointerPressed(e);
FlyoutBase.ShowAttachedFlyout(this);
}
public override void Render(DrawingContext context)
{
base.Render(context);
var brush = Brush ?? Brushes.Black;
context.FillRectangle(brush, Bounds);
var text = (Brush as ISolidColorBrush)?.Color.ToString() ?? "(null)";
var ft = new FormattedText(text,
CultureInfo.CurrentCulture,
FlowDirection.LeftToRight,
Typeface.Default,
10,
GetTextBrush(brush));
context.DrawText(ft,
new Point(Bounds.Width / 2 - ft.Width / 2, Bounds.Height / 2 - ft.Height / 2));
}
/// <summary>
/// Get Contrasted Text Color
/// </summary>
/// <param name="brush"></param>
/// <returns></returns>
private static IBrush GetTextBrush(IBrush brush)
{
if (brush is ISolidColorBrush solid)
{
var color = solid.Color;
var l = ColorHelper.GetRelativeLuminance(color);
return l < 0.5 ? Brushes.White : Brushes.Black;
}
return Brushes.White;
}
}
}

94
src/Avalonia.Diagnostics/Diagnostics/Controls/BrushEditor.cs

@ -1,94 +0,0 @@
using System.Globalization;
using Avalonia.Controls;
using Avalonia.Controls.Primitives;
using Avalonia.Input;
using Avalonia.Media;
using Avalonia.Media.Immutable;
namespace Avalonia.Diagnostics.Controls
{
internal sealed class BrushEditor : Control
{
/// <summary>
/// Defines the <see cref="Brush" /> property.
/// </summary>
public static readonly DirectProperty<BrushEditor, IBrush?> BrushProperty =
AvaloniaProperty.RegisterDirect<BrushEditor, IBrush?>(
nameof(Brush), o => o.Brush, (o, v) => o.Brush = v);
private IBrush? _brush;
public IBrush? Brush
{
get => _brush;
set => SetAndRaise(BrushProperty, ref _brush, value);
}
protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change)
{
base.OnPropertyChanged(change);
if (change.Property == BrushProperty)
{
switch (Brush)
{
case ISolidColorBrush scb:
{
var colorView = new ColorView
{
HexInputAlphaPosition = AlphaComponentPosition.Leading, // Always match XAML
Color = scb.Color,
};
colorView.ColorChanged += (_, e) => Brush = new ImmutableSolidColorBrush(e.NewColor);
FlyoutBase.SetAttachedFlyout(this, new Flyout { Content = colorView });
ToolTip.SetTip(this, $"{scb.Color} ({Brush.GetType().Name})");
break;
}
default:
FlyoutBase.SetAttachedFlyout(this, null);
ToolTip.SetTip(this, Brush?.GetType().Name ?? "(null)");
break;
}
InvalidateVisual();
}
}
protected override void OnPointerPressed(PointerPressedEventArgs e)
{
base.OnPointerPressed(e);
FlyoutBase.ShowAttachedFlyout(this);
}
public override void Render(DrawingContext context)
{
base.Render(context);
if (Brush != null)
{
context.FillRectangle(Brush, Bounds);
}
else
{
context.FillRectangle(Brushes.Black, Bounds);
var ft = new FormattedText("(null)",
CultureInfo.CurrentCulture,
FlowDirection.LeftToRight,
Typeface.Default,
10,
Brushes.White);
context.DrawText(ft,
new Point(Bounds.Width / 2 - ft.Width / 2, Bounds.Height / 2 - ft.Height / 2));
}
}
}
}

1
src/Avalonia.Diagnostics/Diagnostics/ViewModels/MainViewModel.cs

@ -44,6 +44,7 @@ namespace Avalonia.Diagnostics.ViewModels
SelectedTab = 0;
if (root is TopLevel topLevel)
{
_pointerOverRoot = topLevel;
_pointerOverSubscription = topLevel.GetObservable(TopLevel.PointerOverElementProperty)
.Subscribe(x => PointerOverElement = x);

1
src/Avalonia.Diagnostics/Diagnostics/Views/MainWindow.xaml

@ -15,6 +15,7 @@
<StyleInclude Source="avares://Avalonia.Controls.DataGrid/Themes/Simple.xaml"/>
<StyleInclude Source="avares://Avalonia.Diagnostics/Diagnostics/Controls/ThicknessEditor.axaml" />
<StyleInclude Source="avares://Avalonia.Diagnostics/Diagnostics/Controls/FilterTextBox.axaml" />
<StyleInclude Source="avares://Avalonia.Diagnostics/Diagnostics/Controls/BrushEditor.axaml" />
<StyleInclude Source="avares://Avalonia.Controls.ColorPicker/Themes/Simple/Simple.xaml" />
</Window.Styles>
<Window.KeyBindings>

20
src/Avalonia.Themes.Fluent/Controls/ToggleSwitch.xaml

@ -28,6 +28,14 @@
<Setter Property="HorizontalContentAlignment" Value="Left" />
<Setter Property="VerticalContentAlignment" Value="Center" />
<Setter Property="FontSize" Value="{DynamicResource ControlContentThemeFontSize}" />
<Setter Property="KnobTransitions">
<Transitions>
<DoubleTransition
Easing="CubicEaseOut"
Property="Canvas.Left"
Duration="0:0:0.2" />
</Transitions>
</Setter>
<Setter Property="Template">
<ControlTemplate>
<Grid Background="{TemplateBinding Background}" RowDefinitions="Auto,*">
@ -134,18 +142,6 @@
<Setter Property="Margin" Value="0" />
</Style>
<!-- NormalState -->
<Style Selector="^:not(:dragging) /template/ Grid#PART_MovingKnobs">
<Setter Property="Transitions">
<Transitions>
<DoubleTransition
Easing="CubicEaseOut"
Property="Canvas.Left"
Duration="0:0:0.2" />
</Transitions>
</Setter>
</Style>
<!-- PointerOverState -->
<Style Selector="^:pointerover /template/ Border#OuterBorder">
<Setter Property="BorderBrush" Value="{DynamicResource ToggleSwitchStrokeOffPointerOver}" />

13
src/Avalonia.Themes.Simple/Controls/ToggleSwitch.xaml

@ -46,6 +46,14 @@
<Setter Property="HorizontalContentAlignment" Value="Left" />
<Setter Property="VerticalContentAlignment" Value="Center" />
<Setter Property="FontSize" Value="{DynamicResource FontSizeNormal}" />
<Setter Property="KnobTransitions">
<Transitions>
<DoubleTransition
Easing="CubicEaseOut"
Property="Canvas.Left"
Duration="0:0:0.2" />
</Transitions>
</Setter>
<Setter Property="Template">
<ControlTemplate>
<Grid Background="{TemplateBinding Background}"
@ -123,11 +131,6 @@
<Grid x:Name="PART_MovingKnobs"
Width="20" Height="20">
<Grid.Transitions>
<Transitions>
<DoubleTransition Property="Canvas.Left" Duration="0:0:0.2" Easing="CubicEaseOut" />
</Transitions>
</Grid.Transitions>
<Ellipse x:Name="SwitchKnobOn"
Fill="{DynamicResource HighlightForegroundColor}"

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

@ -1,5 +1,6 @@
using System;
using System.Diagnostics;
using System.IO;
using System.Threading;
using Avalonia;
using Avalonia.Controls;
@ -22,13 +23,30 @@ using Avalonia.Threading;
namespace Avalonia.LinuxFramebuffer
{
internal class LinuxFramebufferIconLoaderStub : IPlatformIconLoader
{
private class IconStub : IWindowIconImpl
{
public void Save(Stream outputStream)
{
}
}
public IWindowIconImpl LoadIcon(string fileName) => new IconStub();
public IWindowIconImpl LoadIcon(Stream stream) => new IconStub();
public IWindowIconImpl LoadIcon(IBitmapImpl bitmap) => new IconStub();
}
class LinuxFramebufferPlatform
{
IOutputBackend _fb;
public static ManualRawEventGrouperDispatchQueue EventGrouperDispatchQueue = new();
internal static Compositor Compositor { get; private set; } = null!;
LinuxFramebufferPlatform(IOutputBackend backend)
{
@ -47,6 +65,7 @@ namespace Avalonia.LinuxFramebuffer
.Bind<IRenderTimer>().ToConstant(new DefaultRenderTimer(opts.Fps))
.Bind<ICursorFactory>().ToTransient<CursorFactoryStub>()
.Bind<IKeyboardDevice>().ToConstant(new KeyboardDevice())
.Bind<IPlatformIconLoader>().ToSingleton<LinuxFramebufferIconLoaderStub>()
.Bind<IPlatformSettings>().ToSingleton<DefaultPlatformSettings>()
.Bind<PlatformHotkeyConfiguration>().ToSingleton<PlatformHotkeyConfiguration>();

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

@ -9,8 +9,6 @@
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\..\Avalonia.DesignerSupport\Avalonia.DesignerSupport.csproj" />
<ProjectReference Include="..\..\Markup\Avalonia.Markup.Xaml\Avalonia.Markup.Xaml.csproj" />
<ProjectReference Include="..\..\Markup\Avalonia.Markup\Avalonia.Markup.csproj" />
<ProjectReference Include="..\..\Avalonia.Base\Avalonia.Base.csproj" />
<ProjectReference Include="..\..\Avalonia.Controls\Avalonia.Controls.csproj" />
<ProjectReference Include="..\..\Avalonia.Diagnostics\Avalonia.Diagnostics.csproj" />

33
tests/Avalonia.Controls.UnitTests/TextBlockTests.cs

@ -115,6 +115,39 @@ namespace Avalonia.Controls.UnitTests
}
}
[Fact]
public void Changing_Inlines_Should_Reset_InlineUIContainer_VisualParent_On_Measure()
{
using (UnitTestApplication.Start(TestServices.MockPlatformRenderInterface))
{
var target = new TextBlock();
var control = new Control();
var run = new InlineUIContainer(control);
target.Inlines.Add(run);
target.Measure(Size.Infinity);
Assert.True(target.IsMeasureValid);
Assert.Equal(target, control.VisualParent);
target.Inlines = null;
Assert.Null(run.Parent);
target.Inlines = new InlineCollection { new Run("Hello World") };
Assert.Null(run.Parent);
target.Measure(Size.Infinity);
Assert.Null(control.VisualParent);
}
}
[Fact]
public void InlineUIContainer_Child_Schould_Be_Arranged()
{

15
tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextLayoutTests.cs

@ -1112,6 +1112,21 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting
}
}
[Fact]
public void Should_HitTestTextPosition_EndOfLine_RTL()
{
var text = "גש\r\n";
using (Start())
{
var textLayout = new TextLayout(text, Typeface.Default, 12, Brushes.Black, flowDirection: FlowDirection.RightToLeft);
var rect = textLayout.HitTestTextPosition(text.Length);
Assert.Equal(14.0625, rect.Top);
}
}
private static IDisposable Start()
{

33
tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextLineTests.cs

@ -2,12 +2,10 @@
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Runtime.InteropServices;
using Avalonia.Headless;
using Avalonia.Media;
using Avalonia.Media.TextFormatting;
using Avalonia.UnitTests;
using Avalonia.Utilities;
using Xunit;
namespace Avalonia.Skia.UnitTests.Media.TextFormatting
@ -1072,7 +1070,7 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting
}
[Fact]
public void Should_GetTextBounds_BiDi()
public void Should_GetTextBounds_Bidi()
{
var text = "אבגדה 12345 ABCDEF אבגדה";
@ -1114,12 +1112,39 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting
bounds = textLine.GetTextBounds(0, 25);
Assert.Equal(5, bounds.Count);
Assert.Equal(4, bounds.Count);
Assert.Equal(textLine.WidthIncludingTrailingWhitespace, bounds.Last().Rectangle.Right);
}
}
[Fact]
public void Should_GetTextBounds_Bidi_2()
{
var text = "אבג ABC אבג 123";
using (Start())
{
var defaultProperties = new GenericTextRunProperties(Typeface.Default);
var textSource = new SingleBufferTextSource(text, defaultProperties, true);
var formatter = new TextFormatterImpl();
var textLine =
formatter.FormatLine(textSource, 0, double.PositiveInfinity,
new GenericTextParagraphProperties(FlowDirection.LeftToRight, TextAlignment.Left,
true, true, defaultProperties, TextWrapping.NoWrap, 0, 0, 0));
var bounds = textLine.GetTextBounds(0, text.Length);
Assert.Equal(4, bounds.Count);
var right = bounds.Last().Rectangle.Right;
Assert.Equal(textLine.WidthIncludingTrailingWhitespace, right);
}
}
private class FixedRunsTextSource : ITextSource
{
private readonly IReadOnlyList<TextRun> _textRuns;

Loading…
Cancel
Save