diff --git a/Avalonia.sln b/Avalonia.sln
index 0bae725d33..107c55aa6f 100644
--- a/Avalonia.sln
+++ b/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}
diff --git a/samples/MobileSandbox.Browser/Logo.svg b/samples/MobileSandbox.Browser/Logo.svg
new file mode 100644
index 0000000000..9685a23af1
--- /dev/null
+++ b/samples/MobileSandbox.Browser/Logo.svg
@@ -0,0 +1,5 @@
+
diff --git a/samples/MobileSandbox.Browser/MobileSandbox.Browser.csproj b/samples/MobileSandbox.Browser/MobileSandbox.Browser.csproj
new file mode 100644
index 0000000000..001fe94c7f
--- /dev/null
+++ b/samples/MobileSandbox.Browser/MobileSandbox.Browser.csproj
@@ -0,0 +1,43 @@
+
+
+ net7.0
+ browser-wasm
+ main.js
+ Exe
+ true
+ true
+ true
+ -sVERBOSE -sERROR_ON_UNDEFINED_SYMBOLS=0
+
+
+
+ true
+ true
+ full
+ true
+ true
+ -O2
+ -O2
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/samples/MobileSandbox.Browser/Program.cs b/samples/MobileSandbox.Browser/Program.cs
new file mode 100644
index 0000000000..cdd986d6e8
--- /dev/null
+++ b/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();
+}
diff --git a/samples/MobileSandbox.Browser/Properties/launchSettings.json b/samples/MobileSandbox.Browser/Properties/launchSettings.json
new file mode 100644
index 0000000000..66234a3211
--- /dev/null
+++ b/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;"
+ }
+ }
+}
diff --git a/samples/MobileSandbox.Browser/Roots.xml b/samples/MobileSandbox.Browser/Roots.xml
new file mode 100644
index 0000000000..5255ae3ef8
--- /dev/null
+++ b/samples/MobileSandbox.Browser/Roots.xml
@@ -0,0 +1,7 @@
+
+
+
+
+
+
+
diff --git a/samples/MobileSandbox.Browser/app.css b/samples/MobileSandbox.Browser/app.css
new file mode 100644
index 0000000000..0e6ab12461
--- /dev/null
+++ b/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;
+ }
+}
diff --git a/samples/MobileSandbox.Browser/favicon.ico b/samples/MobileSandbox.Browser/favicon.ico
new file mode 100644
index 0000000000..da8d49ff9b
Binary files /dev/null and b/samples/MobileSandbox.Browser/favicon.ico differ
diff --git a/samples/MobileSandbox.Browser/index.html b/samples/MobileSandbox.Browser/index.html
new file mode 100644
index 0000000000..32ab8628fb
--- /dev/null
+++ b/samples/MobileSandbox.Browser/index.html
@@ -0,0 +1,31 @@
+
+
+
+
+
+
+ Mobile Sandbox
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/samples/MobileSandbox.Browser/main.js b/samples/MobileSandbox.Browser/main.js
new file mode 100644
index 0000000000..9d90db8bd2
--- /dev/null
+++ b/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!"]);
diff --git a/samples/MobileSandbox.Browser/runtimeconfig.template.json b/samples/MobileSandbox.Browser/runtimeconfig.template.json
new file mode 100644
index 0000000000..8f0557352c
--- /dev/null
+++ b/samples/MobileSandbox.Browser/runtimeconfig.template.json
@@ -0,0 +1,11 @@
+{
+ "wasmHostProperties": {
+ "perHostConfig": [
+ {
+ "name": "browser",
+ "html-path": "index.html",
+ "Host": "browser"
+ }
+ ]
+ }
+}
diff --git a/src/Avalonia.Base/Media/GlyphRun.cs b/src/Avalonia.Base/Media/GlyphRun.cs
index 350d8817f1..0f70386424 100644
--- a/src/Avalonia.Base/Media/GlyphRun.cs
+++ b/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;
diff --git a/src/Avalonia.Base/Media/TextFormatting/TextFormatterImpl.cs b/src/Avalonia.Base/Media/TextFormatting/TextFormatterImpl.cs
index a609800fb8..d2198a2cbf 100644
--- a/src/Avalonia.Base/Media/TextFormatting/TextFormatterImpl.cs
+++ b/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;
}
diff --git a/src/Avalonia.Base/Media/TextFormatting/TextLayout.cs b/src/Avalonia.Base/Media/TextFormatting/TextLayout.cs
index 4ccb3f6a37..b6b6d11a49 100644
--- a/src/Avalonia.Base/Media/TextFormatting/TextLayout.cs
+++ b/src/Avalonia.Base/Media/TextFormatting/TextLayout.cs
@@ -128,7 +128,7 @@ namespace Avalonia.Media.TextFormatting
///
/// Gets the text spacing.
///
- public double LetterSpacing => _paragraphProperties.LetterSpacing;
+ public double LetterSpacing => _paragraphProperties.LetterSpacing;
///
/// 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;
diff --git a/src/Avalonia.Base/Media/TextFormatting/TextLineImpl.cs b/src/Avalonia.Base/Media/TextFormatting/TextLineImpl.cs
index c2ec78e187..ca31d9a6d0 100644
--- a/src/Avalonia.Base/Media/TextFormatting/TextLineImpl.cs
+++ b/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);
+ }
}
///
diff --git a/src/Avalonia.Controls/Primitives/OverlayLayer.cs b/src/Avalonia.Controls/Primitives/OverlayLayer.cs
index 76b56f3a11..f9542c883a 100644
--- a/src/Avalonia.Controls/Primitives/OverlayLayer.cs
+++ b/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)
{
diff --git a/src/tools/Avalonia.Designer.HostApp/Avalonia.Designer.HostApp.csproj b/src/tools/Avalonia.Designer.HostApp/Avalonia.Designer.HostApp.csproj
index f3af312d1a..ae2d3dd847 100644
--- a/src/tools/Avalonia.Designer.HostApp/Avalonia.Designer.HostApp.csproj
+++ b/src/tools/Avalonia.Designer.HostApp/Avalonia.Designer.HostApp.csproj
@@ -9,8 +9,6 @@
-
-
diff --git a/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextLayoutTests.cs b/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextLayoutTests.cs
index 71aeb4397e..ac444e37df 100644
--- a/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextLayoutTests.cs
+++ b/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()
{
diff --git a/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextLineTests.cs b/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextLineTests.cs
index 1d07e780e6..12427e1f9e 100644
--- a/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextLineTests.cs
+++ b/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 _textRuns;