Просмотр исходного кода

Merge remote-tracking branch 'origin/master' into features/blazorless-wasm

# Conflicts:
#	azure-pipelines.yml
#	global.json
Dan Walmsley 3 лет назад
Родитель
Сommit
72e1c7fe9b
35 измененных файлов с 864 добавлено и 310 удалено
  1. 3 0
      .gitmodules
  2. 0 0
      .nuke
  3. 148 0
      .nuke/build.schema.json
  4. 4 0
      .nuke/parameters.json
  5. 2 2
      azure-pipelines-integrationtests.yml
  6. 1 50
      azure-pipelines.yml
  7. 7 0
      build.cmd
  8. 24 26
      build.ps1
  9. 46 14
      build.sh
  10. 2 1
      dirs.proj
  11. 4 0
      global.json
  12. 3 8
      nukebuild/Build.cs
  13. 2 2
      nukebuild/BuildParameters.cs
  14. 2 2
      nukebuild/DotNetConfigHelper.cs
  15. 15 0
      nukebuild/Shims.cs
  16. 19 15
      nukebuild/_build.csproj
  17. 1 0
      nukebuild/il-repack
  18. 3 0
      samples/ControlCatalog/MainView.xaml
  19. 2 3
      samples/ControlCatalog/MainView.xaml.cs
  20. 66 0
      samples/ControlCatalog/Pages/AdornerLayerPage.xaml
  21. 48 0
      samples/ControlCatalog/Pages/AdornerLayerPage.xaml.cs
  22. 7 5
      samples/ControlCatalog/Pages/ScreenPage.cs
  23. 5 0
      samples/RenderDemo/Pages/FormattedTextPage.axaml.cs
  24. 0 1
      samples/Sandbox/MainWindow.axaml
  25. 102 4
      src/Avalonia.Base/Media/FormattedText.cs
  26. 13 0
      src/Avalonia.Base/Media/Geometry.cs
  27. 6 5
      src/Avalonia.Base/Media/GlyphRun.cs
  28. 117 136
      src/Avalonia.Base/Media/TextFormatting/TextLineImpl.cs
  29. 1 1
      src/Avalonia.Base/Media/TextFormatting/TextRunBounds.cs
  30. 1 1
      src/Avalonia.Build.Tasks/Avalonia.Build.Tasks.csproj
  31. 12 2
      src/Avalonia.Controls/Documents/InlineCollection.cs
  32. 111 0
      src/Avalonia.Controls/Primitives/AdornerLayer.cs
  33. 14 20
      src/Avalonia.Controls/Window.cs
  34. 1 1
      src/Avalonia.X11/X11Window.cs
  35. 72 11
      tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextLineTests.cs

+ 3 - 0
.gitmodules

@@ -4,3 +4,6 @@
 [submodule "src/Markup/Avalonia.Markup.Xaml/XamlIl/xamlil.github"]
 	path = src/Markup/Avalonia.Markup.Xaml.Loader/xamlil.github
 	url = https://github.com/kekekeks/XamlX.git
+[submodule "nukebuild/il-repack"]
+	path = nukebuild/il-repack
+	url = https://github.com/Gillibald/il-repack


+ 148 - 0
.nuke/build.schema.json

@@ -0,0 +1,148 @@
+{
+  "$schema": "http://json-schema.org/draft-04/schema#",
+  "title": "Build Schema",
+  "$ref": "#/definitions/build",
+  "definitions": {
+    "build": {
+      "type": "object",
+      "properties": {
+        "Configuration": {
+          "type": "string",
+          "description": "configuration"
+        },
+        "Continue": {
+          "type": "boolean",
+          "description": "Indicates to continue a previously failed build attempt"
+        },
+        "ForceNugetVersion": {
+          "type": "string",
+          "description": "force-nuget-version"
+        },
+        "Help": {
+          "type": "boolean",
+          "description": "Shows the help text for this build assembly"
+        },
+        "Host": {
+          "type": "string",
+          "description": "Host for execution. Default is 'automatic'",
+          "enum": [
+            "AppVeyor",
+            "AzurePipelines",
+            "Bamboo",
+            "Bitbucket",
+            "Bitrise",
+            "GitHubActions",
+            "GitLab",
+            "Jenkins",
+            "Rider",
+            "SpaceAutomation",
+            "TeamCity",
+            "Terminal",
+            "TravisCI",
+            "VisualStudio",
+            "VSCode"
+          ]
+        },
+        "NoLogo": {
+          "type": "boolean",
+          "description": "Disables displaying the NUKE logo"
+        },
+        "Partition": {
+          "type": "string",
+          "description": "Partition to use on CI"
+        },
+        "Plan": {
+          "type": "boolean",
+          "description": "Shows the execution plan (HTML)"
+        },
+        "Profile": {
+          "type": "array",
+          "description": "Defines the profiles to load",
+          "items": {
+            "type": "string"
+          }
+        },
+        "Root": {
+          "type": "string",
+          "description": "Root directory during build execution"
+        },
+        "Skip": {
+          "type": "array",
+          "description": "List of targets to be skipped. Empty list skips all dependencies",
+          "items": {
+            "type": "string",
+            "enum": [
+              "CiAzureLinux",
+              "CiAzureOSX",
+              "CiAzureWindows",
+              "Clean",
+              "Compile",
+              "CompileHtmlPreviewer",
+              "CompileNative",
+              "CreateIntermediateNugetPackages",
+              "CreateNugetPackages",
+              "GenerateCppHeaders",
+              "Package",
+              "RunCoreLibsTests",
+              "RunDesignerTests",
+              "RunHtmlPreviewerTests",
+              "RunLeakTests",
+              "RunRenderTests",
+              "RunTests",
+              "ZipFiles"
+            ]
+          }
+        },
+        "SkipPreviewer": {
+          "type": "boolean",
+          "description": "skip-previewer"
+        },
+        "SkipTests": {
+          "type": "boolean",
+          "description": "skip-tests"
+        },
+        "Solution": {
+          "type": "string",
+          "description": "Path to a solution file that is automatically loaded. Default is Avalonia.sln"
+        },
+        "Target": {
+          "type": "array",
+          "description": "List of targets to be invoked. Default is '{default_target}'",
+          "items": {
+            "type": "string",
+            "enum": [
+              "CiAzureLinux",
+              "CiAzureOSX",
+              "CiAzureWindows",
+              "Clean",
+              "Compile",
+              "CompileHtmlPreviewer",
+              "CompileNative",
+              "CreateIntermediateNugetPackages",
+              "CreateNugetPackages",
+              "GenerateCppHeaders",
+              "Package",
+              "RunCoreLibsTests",
+              "RunDesignerTests",
+              "RunHtmlPreviewerTests",
+              "RunLeakTests",
+              "RunRenderTests",
+              "RunTests",
+              "ZipFiles"
+            ]
+          }
+        },
+        "Verbosity": {
+          "type": "string",
+          "description": "Logging verbosity during build execution. Default is 'Normal'",
+          "enum": [
+            "Minimal",
+            "Normal",
+            "Quiet",
+            "Verbose"
+          ]
+        }
+      }
+    }
+  }
+}

+ 4 - 0
.nuke/parameters.json

@@ -0,0 +1,4 @@
+{
+  "$schema": "./build.schema.json",
+  "Solution": ""
+}

+ 2 - 2
azure-pipelines-integrationtests.yml

@@ -41,9 +41,9 @@ jobs:
 
   steps:
   - task: UseDotNet@2
-    displayName: 'Use .NET Core SDK 6.0.202'
+    displayName: 'Use .NET Core SDK 6.0.401'
     inputs:
-      version: 6.0.202
+      version: 6.0.401
 
   - task: Windows Application Driver@0
     inputs:

+ 1 - 50
azure-pipelines.yml

@@ -29,27 +29,11 @@ jobs:
   pool:
     vmImage: 'ubuntu-20.04'
   steps:
-  - task: UseDotNet@2
-    displayName: 'Use .NET Core SDK 3.1.418'
-    inputs:
-      version: 3.1.418
-
   - task: UseDotNet@2
     displayName: 'Use .NET Core SDK 6.0.401'
     inputs:
       version: 6.0.401
 
-  - task: UseDotNet@2
-    displayName: 'Use .NET Core SDK 7.0.100-rc.1.22431.12'
-    inputs:
-      version: 7.0.100-rc.1.22431.12
-
-  - task: CmdLine@2
-    displayName: 'Install Workloads'
-    inputs:
-      script: |
-       dotnet workload install wasm-tools wasm-experimental
-
   - task: CmdLine@2
     displayName: 'Run Build'
     inputs:
@@ -71,34 +55,11 @@ jobs:
   pool:
     vmImage: 'macos-12'
   steps:
-  - task: UseDotNet@2
-    displayName: 'Use .NET Core SDK 3.1.418'
-    inputs:
-      version: 3.1.418
-
   - task: UseDotNet@2
     displayName: 'Use .NET Core SDK 6.0.401'
     inputs:
       version: 6.0.401
-
-  - task: UseDotNet@2
-    displayName: 'Use .NET Core SDK 7.0.100-rc.1.22431.12'
-    inputs:
-      version: 7.0.100-rc.1.22431.12
-
-  - task: CmdLine@2
-    displayName: 'Install Workloads'
-    inputs:
-      script: |
-       dotnet workload install android ios wasm-tools wasm-experimental
       
-  - task: CmdLine@2
-    displayName: 'Install Mono 5.18'
-    inputs:
-      script: |
-        curl -o ./mono.pkg https://download.mono-project.com/archive/5.18.0/macos-10-universal/MonoFramework-MDK-5.18.0.225.macos10.xamarin.universal.pkg 
-        sudo installer -verbose -pkg ./mono.pkg -target /
-
   - task: CmdLine@2
     displayName: 'Generate avalonia-native'
     inputs:
@@ -154,21 +115,11 @@ jobs:
   variables:
     SolutionDir: '$(Build.SourcesDirectory)'
   steps:
-  - task: UseDotNet@2
-    displayName: 'Use .NET Core SDK 3.1.418'
-    inputs:
-      version: 3.1.418
-
   - task: UseDotNet@2
     displayName: 'Use .NET Core SDK 6.0.401'
     inputs:
       version: 6.0.401
 
-  - task: UseDotNet@2
-    displayName: 'Use .NET Core SDK 7.0.100-rc.1.22431.12'
-    inputs:
-      version: 7.0.100-rc.1.22431.12
-
   - task: CmdLine@2
     displayName: 'Install Workloads'
     inputs:
@@ -179,7 +130,7 @@ jobs:
     displayName: 'Install Nuke'
     inputs:
       script: |
-       dotnet tool install --global Nuke.GlobalTool --version 0.24.0 
+       dotnet tool install --global Nuke.GlobalTool --version 6.2.1 
 
   - task: CmdLine@2
     displayName: 'Run Nuke'

+ 7 - 0
build.cmd

@@ -0,0 +1,7 @@
+:; set -eo pipefail
+:; SCRIPT_DIR=$(cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd)
+:; ${SCRIPT_DIR}/build.sh "$@"
+:; exit $?
+
+@ECHO OFF
+powershell -ExecutionPolicy ByPass -NoProfile -File "%~dp0build.ps1" %*

+ 24 - 26
build.ps1

@@ -1,13 +1,12 @@
 [CmdletBinding()]
 Param(
-    #[switch]$CustomParam,
     [Parameter(Position=0,Mandatory=$false,ValueFromRemainingArguments=$true)]
     [string[]]$BuildArguments
 )
 
-Write-Output "Windows PowerShell $($Host.Version)"
+Write-Output "PowerShell $($PSVersionTable.PSEdition) version $($PSVersionTable.PSVersion)"
 
-Set-StrictMode -Version 2.0; $ErrorActionPreference = "Stop"; $ConfirmPreference = "None"; trap { exit 1 }
+Set-StrictMode -Version 2.0; $ErrorActionPreference = "Stop"; $ConfirmPreference = "None"; trap { Write-Error $_ -ErrorAction Continue; exit 1 }
 $PSScriptRoot = Split-Path $MyInvocation.MyCommand.Path -Parent
 
 ###########################################################################
@@ -15,15 +14,15 @@ $PSScriptRoot = Split-Path $MyInvocation.MyCommand.Path -Parent
 ###########################################################################
 
 $BuildProjectFile = "$PSScriptRoot\nukebuild\_build.csproj"
-$TempDirectory = "$PSScriptRoot\\.tmp"
+$TempDirectory = "$PSScriptRoot\\.nuke\temp"
 
 $DotNetGlobalFile = "$PSScriptRoot\\global.json"
-$DotNetInstallUrl = "https://raw.githubusercontent.com/dotnet/cli/master/scripts/obtain/dotnet-install.ps1"
+$DotNetInstallUrl = "https://dot.net/v1/dotnet-install.ps1"
 $DotNetChannel = "Current"
 
 $env:DOTNET_SKIP_FIRST_TIME_EXPERIENCE = 1
 $env:DOTNET_CLI_TELEMETRY_OPTOUT = 1
-$env:NUGET_XMLDOC_MODE = "skip"
+$env:DOTNET_MULTILEVEL_LOOKUP = 0
 
 ###########################################################################
 # EXECUTION
@@ -34,38 +33,37 @@ function ExecSafe([scriptblock] $cmd) {
     if ($LASTEXITCODE) { exit $LASTEXITCODE }
 }
 
-# If global.json exists, load expected version
-if (Test-Path $DotNetGlobalFile) {
-    $DotNetGlobal = $(Get-Content $DotNetGlobalFile | Out-String | ConvertFrom-Json)
-    if ($DotNetGlobal.PSObject.Properties["sdk"] -and $DotNetGlobal.sdk.PSObject.Properties["version"]) {
-        $DotNetVersion = $DotNetGlobal.sdk.version
-    }
-}
-
-# If dotnet is installed locally, and expected version is not set or installation matches the expected version
+# If dotnet CLI is installed globally and it matches requested version, use for execution
 if ($null -ne (Get-Command "dotnet" -ErrorAction SilentlyContinue) -and `
-     (!(Test-Path variable:DotNetVersion) -or $(& dotnet --version) -eq $DotNetVersion)) {
+     $(dotnet --version) -and $LASTEXITCODE -eq 0) {
     $env:DOTNET_EXE = (Get-Command "dotnet").Path
 }
 else {
-    $DotNetDirectory = "$TempDirectory\dotnet-win"
-    $env:DOTNET_EXE = "$DotNetDirectory\dotnet.exe"
-
     # Download install script
     $DotNetInstallFile = "$TempDirectory\dotnet-install.ps1"
-    mkdir -force $TempDirectory > $null
+    New-Item -ItemType Directory -Path $TempDirectory -Force | Out-Null
+    [Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12
     (New-Object System.Net.WebClient).DownloadFile($DotNetInstallUrl, $DotNetInstallFile)
 
+    # If global.json exists, load expected version
+    if (Test-Path $DotNetGlobalFile) {
+        $DotNetGlobal = $(Get-Content $DotNetGlobalFile | Out-String | ConvertFrom-Json)
+        if ($DotNetGlobal.PSObject.Properties["sdk"] -and $DotNetGlobal.sdk.PSObject.Properties["version"]) {
+            $DotNetVersion = $DotNetGlobal.sdk.version
+        }
+    }
+
     # Install by channel or version
+    $DotNetDirectory = "$TempDirectory\dotnet-win"
     if (!(Test-Path variable:DotNetVersion)) {
-        ExecSafe { & $DotNetInstallFile -InstallDir $DotNetDirectory -Channel $DotNetChannel -NoPath }
+        ExecSafe { & powershell $DotNetInstallFile -InstallDir $DotNetDirectory -Channel $DotNetChannel -NoPath }
     } else {
-        ExecSafe { & $DotNetInstallFile -InstallDir $DotNetDirectory -Version $DotNetVersion -NoPath }
+        ExecSafe { & powershell $DotNetInstallFile -InstallDir $DotNetDirectory -Version $DotNetVersion -NoPath }
     }
-
-    $env:PATH="$DotNetDirectory;$env:PATH"
+    $env:DOTNET_EXE = "$DotNetDirectory\dotnet.exe"
 }
 
-Write-Output "Microsoft (R) .NET Core SDK version $(& $env:DOTNET_EXE --version)"
+Write-Output "Microsoft (R) .NET SDK version $(& $env:DOTNET_EXE --version)"
 
-ExecSafe { & $env:DOTNET_EXE run --project $BuildProjectFile -- $BuildArguments }
+ExecSafe { & $env:DOTNET_EXE build $BuildProjectFile /nodeReuse:false /p:UseSharedCompilation=false -nologo -clp:NoSummary --verbosity quiet }
+ExecSafe { & $env:DOTNET_EXE run --project $BuildProjectFile --no-build -- $BuildArguments }

+ 46 - 14
build.sh

@@ -1,16 +1,6 @@
 #!/usr/bin/env bash
 
-echo $(bash --version 2>&1 | head -n 1)
-
-#CUSTOMPARAM=0
-BUILD_ARGUMENTS=()
-for i in "$@"; do
-    case $(echo $1 | awk '{print tolower($0)}') in
-        # -custom-param) CUSTOMPARAM=1;;
-        *) BUILD_ARGUMENTS+=("$1") ;;
-    esac
-    shift
-done
+bash --version 2>&1 | head -n 1
 
 set -eo pipefail
 SCRIPT_DIR=$(cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd)
@@ -20,11 +10,53 @@ SCRIPT_DIR=$(cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd)
 ###########################################################################
 
 BUILD_PROJECT_FILE="$SCRIPT_DIR/nukebuild/_build.csproj"
+TEMP_DIRECTORY="$SCRIPT_DIR//.nuke/temp"
+
+DOTNET_GLOBAL_FILE="$SCRIPT_DIR//global.json"
+DOTNET_INSTALL_URL="https://dot.net/v1/dotnet-install.sh"
+DOTNET_CHANNEL="Current"
 
 export DOTNET_CLI_TELEMETRY_OPTOUT=1
 export DOTNET_SKIP_FIRST_TIME_EXPERIENCE=1
-export NUGET_XMLDOC_MODE="skip"
+export DOTNET_MULTILEVEL_LOOKUP=0
 
-dotnet --info
+###########################################################################
+# EXECUTION
+###########################################################################
 
-dotnet run --project "$BUILD_PROJECT_FILE" -- ${BUILD_ARGUMENTS[@]}
+function FirstJsonValue {
+    perl -nle 'print $1 if m{"'"$1"'": "([^"]+)",?}' <<< "${@:2}"
+}
+
+# If dotnet CLI is installed globally and it matches requested version, use for execution
+if [ -x "$(command -v dotnet)" ] && dotnet --version &>/dev/null; then
+    export DOTNET_EXE="$(command -v dotnet)"
+else
+    # Download install script
+    DOTNET_INSTALL_FILE="$TEMP_DIRECTORY/dotnet-install.sh"
+    mkdir -p "$TEMP_DIRECTORY"
+    curl -Lsfo "$DOTNET_INSTALL_FILE" "$DOTNET_INSTALL_URL"
+    chmod +x "$DOTNET_INSTALL_FILE"
+
+    # If global.json exists, load expected version
+    if [[ -f "$DOTNET_GLOBAL_FILE" ]]; then
+        DOTNET_VERSION=$(FirstJsonValue "version" "$(cat "$DOTNET_GLOBAL_FILE")")
+        if [[ "$DOTNET_VERSION" == ""  ]]; then
+            unset DOTNET_VERSION
+        fi
+    fi
+
+    # Install by channel or version
+    DOTNET_DIRECTORY="$TEMP_DIRECTORY/dotnet-unix"
+    if [[ -z ${DOTNET_VERSION+x} ]]; then
+        "$DOTNET_INSTALL_FILE" --install-dir "$DOTNET_DIRECTORY" --channel "$DOTNET_CHANNEL" --no-path
+    else
+        "$DOTNET_INSTALL_FILE" --install-dir "$DOTNET_DIRECTORY" --version "$DOTNET_VERSION" --no-path
+    fi
+    export DOTNET_EXE="$DOTNET_DIRECTORY/dotnet"
+fi
+
+echo "Microsoft (R) .NET SDK version $("$DOTNET_EXE" --version)"
+
+"$DOTNET_EXE" build "$BUILD_PROJECT_FILE" /nodeReuse:false /p:UseSharedCompilation=false -nologo -clp:NoSummary --verbosity quiet
+"$DOTNET_EXE" run --project "$BUILD_PROJECT_FILE" --no-build -- "$@"

+ 2 - 1
dirs.proj

@@ -23,12 +23,13 @@
     <ProjectReference Remove="samples/ControlCatalog.Desktop/*.*proj" />
   </ItemGroup>
   <!-- Build android and iOS projects only on Windows, where we have installed android workload -->
+
   <ItemGroup Condition="!$([MSBuild]::IsOsPlatform('Windows'))">
     <ProjectReference Remove="src/Android/**/*.*proj" />
     <ProjectReference Remove="src/iOS/**/*.*proj" />
   </ItemGroup>
 
   <ItemGroup>
-    <PackageReference Include="SlnGen" Version="2.0.40" PrivateAssets="all" />
+    <PackageReference Include="Microsoft.VisualStudio.SlnGen" Version="8.5.17" PrivateAssets="all" />
   </ItemGroup>
 </Project>

+ 4 - 0
global.json

@@ -1,4 +1,8 @@
 {
+    "sdk": {
+        "version": "6.0.401",
+        "rollForward": "latestFeature"
+    },
     "msbuild-sdks": {
         "Microsoft.Build.Traversal": "1.0.43",
         "MSBuild.Sdk.Extras": "3.0.22",

+ 3 - 8
nukebuild/Build.cs

@@ -163,7 +163,7 @@ partial class Build : NukeBuild
                 .EnableNoBuild()
                 .EnableNoRestore()
                 .When(Parameters.PublishTestResults, _ => _
-                    .SetLogger("trx")
+                    .SetLoggers("trx")
                     .SetResultsDirectory(Parameters.TestResultsRoot)));
         }
     }
@@ -215,8 +215,6 @@ partial class Build : NukeBuild
             RunCoreTest("Avalonia.DesignerSupport.Tests");
         });
 
-    [PackageExecutable("JetBrains.dotMemoryUnit", "dotMemoryUnit.exe")] readonly Tool DotMemoryUnit;
-
     Target RunLeakTests => _ => _
         .OnlyWhenStatic(() => !Parameters.SkipTests && Parameters.IsRunningOnWindows)
         .DependsOn(Compile)
@@ -224,12 +222,9 @@ partial class Build : NukeBuild
         {
             void DoMemoryTest()
             {
-                var testAssembly = "tests\\Avalonia.LeakTests\\bin\\Release\\net461\\Avalonia.LeakTests.dll";
-                DotMemoryUnit(
-                    $"{XunitPath.DoubleQuoteIfNeeded()} --propagate-exit-code -- {testAssembly}",
-                    timeout: 120_000);
+                RunCoreTest("Avalonia.LeakTests");
             }
-            ControlFlow.ExecuteWithRetry(DoMemoryTest, waitInSeconds: 3);
+            ControlFlow.ExecuteWithRetry(DoMemoryTest, delay: TimeSpan.FromMilliseconds(3));
         });
 
     Target ZipFiles => _ => _

+ 2 - 2
nukebuild/BuildParameters.cs

@@ -74,11 +74,11 @@ public partial class Build
             MSBuildSolution = RootDirectory / "dirs.proj";
 
             // PARAMETERS
-            IsLocalBuild = Host == HostType.Console;
+            IsLocalBuild = NukeBuild.IsLocalBuild;
             IsRunningOnUnix = Environment.OSVersion.Platform == PlatformID.Unix ||
                               Environment.OSVersion.Platform == PlatformID.MacOSX;
             IsRunningOnWindows = RuntimeInformation.IsOSPlatform(OSPlatform.Windows);
-            IsRunningOnAzure = Host == HostType.AzurePipelines ||
+            IsRunningOnAzure = Host is AzurePipelines ||
                                Environment.GetEnvironmentVariable("LOGNAME") == "vsts";
 
             if (IsRunningOnAzure)

+ 2 - 2
nukebuild/DotNetConfigHelper.cs

@@ -46,7 +46,7 @@ public class DotNetConfigHelper
     public DotNetConfigHelper SetVerbosity(DotNetVerbosity verbosity)
     {
         Build = Build?.SetVerbosity(verbosity);
-        Pack = Pack?.SetVerbostiy(verbosity);
+        Pack = Pack?.SetVerbosity(verbosity);
         Test = Test?.SetVerbosity(verbosity);
         return this;
     }
@@ -54,4 +54,4 @@ public class DotNetConfigHelper
     public static implicit operator DotNetConfigHelper(DotNetBuildSettings s) => new DotNetConfigHelper(s);
     public static implicit operator DotNetConfigHelper(DotNetPackSettings s) => new DotNetConfigHelper(s);
     public static implicit operator DotNetConfigHelper(DotNetTestSettings s) => new DotNetConfigHelper(s);
-}
+}

+ 15 - 0
nukebuild/Shims.cs

@@ -49,7 +49,11 @@ public partial class Build
                         {
                             if (fsEntry is FileInfo)
                             {
+#if NET6
                                 var relPath = Path.GetRelativePath(rootPath, fsEntry.FullName);
+#else
+                                var relPath = GetRelativePath(rootPath, fsEntry.FullName);
+#endif
                                 AddFile(fsEntry.FullName, relPath);
                             }
                         }
@@ -78,6 +82,17 @@ public partial class Build
         }
     }
 
+    private static string GetRelativePath(string relativeTo, string path)
+    {
+        var uri = new Uri(relativeTo);
+        var rel = Uri.UnescapeDataString(uri.MakeRelativeUri(new Uri(path)).ToString()).Replace(Path.AltDirectorySeparatorChar, Path.DirectorySeparatorChar);
+        if (rel.Contains(Path.DirectorySeparatorChar.ToString()) == false)
+        {
+            rel = $".{Path.DirectorySeparatorChar}{rel}";
+        }
+        return rel;
+    }
+
     class NumergeNukeLogger : INumergeLogger
     {
         public void Log(NumergeLogLevel level, string message)

+ 19 - 15
nukebuild/_build.csproj

@@ -1,40 +1,44 @@
 <Project Sdk="Microsoft.NET.Sdk">
   <PropertyGroup>
     <OutputType>Exe</OutputType>
-    <TargetFramework>netcoreapp3.1</TargetFramework>
     <AppendTargetFrameworkToOutputPath>false</AppendTargetFrameworkToOutputPath>
     <RootNamespace></RootNamespace>
     <IsPackable>False</IsPackable>
     <NoWarn>CS0649;CS0169</NoWarn>
+    <NukeTelemetryVersion>1</NukeTelemetryVersion>
+    <TargetFramework>net6.0</TargetFramework>
   </PropertyGroup>
+ 
  <Import Project="..\build\JetBrains.dotMemoryUnit.props" />
   <ItemGroup>
-    <PackageReference Include="Nuke.Common" Version="5.0.0" />
-    <PackageReference Include="xunit.runner.console" Version="2.3.1" />
+    <PackageReference Include="Nuke.Common" Version="6.2.1" />
     <PackageReference Include="vswhere" Version="2.6.7" Condition=" '$(OS)' == 'Windows_NT' " />
-    <PackageReference Include="ILRepack.NETStandard" Version="2.0.4" />
     <PackageReference Include="MicroCom.CodeGenerator" Version="0.10.4" />
     <!-- Keep in sync with Avalonia.Build.Tasks -->
-    <PackageReference Include="Mono.Cecil" Version="0.11.2" />
+    <PackageReference Include="Mono.Cecil" Version="0.11.4" />
+   <PackageReference Include="SourceLink" Version="1.1.0" GeneratePathProperty="true" />
+    <PackageReference Include="Microsoft.Build.Framework" Version="17.3.1" PrivateAssets="All" />
+    <PackageReference Include="xunit.runner.console" Version="2.4.2">
+      <PrivateAssets>all</PrivateAssets>
+      <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
+    </PackageReference>
   </ItemGroup>
 
   <ItemGroup>
     <NukeMetadata Include="**\*.json" Exclude="bin\**;obj\**" />
     <NukeExternalFiles Include="**\*.*.ext" Exclude="bin\**;obj\**" />
-    <None Remove="*.csproj.DotSettings;*.ref.*.txt" />
 
     <!-- Common build related files -->
-    <None Include="..\build.ps1" />
-    <None Include="..\build.sh" />
-    <None Include="..\.nuke" />
-    <None Include="..\global.json" Condition="Exists('..\global.json')" />
-    <None Include="..\nuget.config" Condition="Exists('..\nuget.config')" />
-    <None Include="..\Jenkinsfile" Condition="Exists('..\Jenkinsfile')" />
-    <None Include="..\appveyor.yml" Condition="Exists('..\appveyor.yml')" />
-    <None Include="..\.travis.yml" Condition="Exists('..\.travis.yml')" />
-    <None Include="..\GitVersion.yml" Condition="Exists('..\GitVersion.yml')" />
     <Compile Remove="Numerge/**/*.*" />
     <Compile Include="Numerge/Numerge/**/*.cs" />
   </ItemGroup>
 
+  	<ItemGroup>
+		<EmbeddedResource Include="$(NuGetPackageRoot)sourcelink/1.1.0/tools/pdbstr.exe"></EmbeddedResource>
+	</ItemGroup>
+    
+  <ItemGroup>
+    <Compile Remove="il-repack\ILRepack\Application.cs" />
+  </ItemGroup>
+
 </Project>

+ 1 - 0
nukebuild/il-repack

@@ -0,0 +1 @@
+Subproject commit 892f079ea8cb0c178f0a68f53a7a7eac13acdda9

+ 3 - 0
samples/ControlCatalog/MainView.xaml

@@ -19,6 +19,9 @@
       <TabItem Header="Acrylic">
         <pages:AcrylicPage />
       </TabItem>
+      <TabItem Header="AdornerLayer">
+        <pages:AdornerLayerPage />
+      </TabItem>
       <TabItem Header="AutoCompleteBox">
         <pages:AutoCompleteBoxPage />
       </TabItem>

+ 2 - 3
samples/ControlCatalog/MainView.xaml.cs

@@ -2,10 +2,10 @@ using System;
 using System.Collections;
 using Avalonia;
 using Avalonia.Controls;
+using Avalonia.Controls.ApplicationLifetimes;
 using Avalonia.Markup.Xaml;
 using Avalonia.Media;
 using Avalonia.Media.Immutable;
-using Avalonia.Platform;
 using Avalonia.Themes.Fluent;
 using ControlCatalog.Models;
 using ControlCatalog.Pages;
@@ -20,7 +20,7 @@ namespace ControlCatalog
 
             var sideBar = this.Get<TabControl>("Sidebar");
 
-            if (AvaloniaLocator.Current?.GetService<IRuntimePlatform>()?.GetRuntimeInfo().IsDesktop == true)
+            if (Application.Current?.ApplicationLifetime is IClassicDesktopStyleApplicationLifetime)
             {
                 var tabItems = (sideBar.Items as IList);
                 tabItems?.Add(new TabItem()
@@ -28,7 +28,6 @@ namespace ControlCatalog
                     Header = "Screens",
                     Content = new ScreenPage()
                 });
-
             }
 
             var themes = this.Get<ComboBox>("Themes");

+ 66 - 0
samples/ControlCatalog/Pages/AdornerLayerPage.xaml

@@ -0,0 +1,66 @@
+<UserControl x:Class="ControlCatalog.Pages.AdornerLayerPage"
+             xmlns="https://github.com/avaloniaui"
+             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
+             xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
+             xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
+             d:DesignHeight="800"
+             d:DesignWidth="400"
+             mc:Ignorable="d">
+
+  <DockPanel>
+    <Grid ColumnDefinitions="Auto,*" RowDefinitions="Auto" Margin="16" DockPanel.Dock="Top">
+      <TextBlock Grid.Column="0" Grid.Row="0">Rotation</TextBlock>
+      <Slider Name="rotation" Maximum="360" Grid.Column="1" Grid.Row="0"/>
+    </Grid>
+
+    <StackPanel Orientation="Horizontal" HorizontalAlignment="Center" DockPanel.Dock="Top">
+      <Button Name="AddAdorner" 
+              Click="AddAdorner_OnClick"
+              Content="Add Adorner" 
+              Margin="6" />
+      <Button Name="RemoveAdorner" 
+              Click="RemoveAdorner_OnClick"
+              Content="Remove Adorner"
+              Margin="6" />
+    </StackPanel>
+
+    <Grid ColumnDefinitions="24,Auto,24" 
+          RowDefinitions="24,Auto,24"
+          HorizontalAlignment="Center"
+          VerticalAlignment="Center">
+      <Border Background="{DynamicResource SystemAccentColor}" Grid.Column="1" Grid.Row="0"/>
+      <Border Background="{DynamicResource SystemAccentColor}" Grid.Column="0" Grid.Row="1"/>
+      <Border Background="{DynamicResource SystemAccentColor}" Grid.Column="2" Grid.Row="1"/>
+      <Border Background="{DynamicResource SystemAccentColor}" Grid.Column="1" Grid.Row="2"/>
+
+      <LayoutTransformControl Name="layoutTransform" Grid.Column="1" Grid.Row="1">
+        <LayoutTransformControl.LayoutTransform>
+          <RotateTransform Angle="{Binding #rotation.Value}"/>
+        </LayoutTransformControl.LayoutTransform>
+
+        <Button Name="AdornerButton"
+                Content="Adorned Button" 
+                HorizontalAlignment="Stretch" HorizontalContentAlignment="Center"
+                VerticalContentAlignment="Center" VerticalAlignment="Stretch" 
+                Width="200" Height="42">
+          <AdornerLayer.Adorner>
+            <Canvas x:Name="AdornerCanvas"
+                    HorizontalAlignment="Stretch"
+                    VerticalAlignment="Stretch"
+                    Background="Cyan"
+                    IsHitTestVisible="False"
+                    Opacity="0.3"
+                    IsVisible="True">
+              <Line StartPoint="-10000,0" EndPoint="10000,0" Stroke="Cyan" StrokeThickness="1" />
+              <Line StartPoint="-10000,42" EndPoint="10000,42" Stroke="Cyan" StrokeThickness="1" />
+              <Line StartPoint="0,-10000" EndPoint="0,10000" Stroke="Cyan" StrokeThickness="1" />
+              <Line StartPoint="200,-10000" EndPoint="200,10000" Stroke="Cyan" StrokeThickness="1" />
+            </Canvas>
+          </AdornerLayer.Adorner>
+        </Button>
+
+      </LayoutTransformControl>
+    </Grid>
+  </DockPanel>
+
+</UserControl>

+ 48 - 0
samples/ControlCatalog/Pages/AdornerLayerPage.xaml.cs

@@ -0,0 +1,48 @@
+using Avalonia.Controls;
+using Avalonia.Controls.Primitives;
+using Avalonia.Interactivity;
+using Avalonia.Markup.Xaml;
+
+namespace ControlCatalog.Pages
+{
+    public class AdornerLayerPage : UserControl
+    {
+        private Control? _adorner;
+
+        public AdornerLayerPage()
+        {
+            this.InitializeComponent();
+        }
+
+        private void InitializeComponent()
+        {
+            AvaloniaXamlLoader.Load(this);
+        }
+
+        private void RemoveAdorner_OnClick(object? sender, RoutedEventArgs e)
+        {
+            var adornerButton = this.FindControl<Button>("AdornerButton");
+            if (adornerButton is { })
+            {
+                var adorner = AdornerLayer.GetAdorner(adornerButton);
+                if (adorner is { })
+                {
+                    _adorner = adorner;
+                }
+                AdornerLayer.SetAdorner(adornerButton, null);
+            }
+        }
+
+        private void AddAdorner_OnClick(object? sender, RoutedEventArgs e)
+        {
+            var adornerButton = this.FindControl<Button>("AdornerButton");
+            if (adornerButton is { })
+            {
+                if (_adorner is { })
+                {
+                    AdornerLayer.SetAdorner(adornerButton, _adorner);
+                }
+            }
+        }
+    }
+}

+ 7 - 5
samples/ControlCatalog/Pages/ScreenPage.cs

@@ -2,10 +2,10 @@ using System;
 using System.Globalization;
 using Avalonia;
 using Avalonia.Controls;
-using Avalonia.Markup.Xaml;
 using Avalonia.Media;
 using Avalonia.Platform;
 using Avalonia.Rendering;
+using Avalonia.Threading;
 
 namespace ControlCatalog.Pages
 {
@@ -18,8 +18,10 @@ namespace ControlCatalog.Pages
         protected override void OnAttachedToVisualTree(VisualTreeAttachmentEventArgs e)
         {
             base.OnAttachedToVisualTree(e);
-            Window w = (Window)VisualRoot!;
-            w.PositionChanged += (sender, args) => InvalidateVisual();
+            if(VisualRoot is Window w)
+            {
+                w.PositionChanged += (_, _) => InvalidateVisual();
+            }
         }
 
         public override void Render(DrawingContext context)
@@ -27,7 +29,7 @@ namespace ControlCatalog.Pages
             base.Render(context);
             if (!(VisualRoot is Window w))
             {
-                return;                
+                return;
             }
             var screens = w.Screens.All;
             var scaling = ((IRenderRoot)w).RenderScaling;
@@ -40,7 +42,7 @@ namespace ControlCatalog.Pages
                     if (screen.Bounds.X / 10f < _leftMost)
                     {
                         _leftMost = screen.Bounds.X / 10f;
-                        InvalidateVisual();
+                        Dispatcher.UIThread.Post(InvalidateVisual, DispatcherPriority.Background);
                         return;
                     }
 

+ 5 - 0
samples/RenderDemo/Pages/FormattedTextPage.axaml.cs

@@ -3,6 +3,7 @@ using Avalonia;
 using Avalonia.Controls;
 using Avalonia.Markup.Xaml;
 using Avalonia.Media;
+using Avalonia.Media.Immutable;
 
 namespace RenderDemo.Pages
 {
@@ -59,6 +60,10 @@ namespace RenderDemo.Pages
             var geometry = formattedText.BuildGeometry(new Point(10 + formattedText.Width + 10, 0));
 
             context.DrawGeometry(gradient, null, geometry);
+
+            var highlightGeometry = formattedText.BuildHighlightGeometry(new Point(10 + formattedText.Width + 10, 0));
+
+            context.DrawGeometry(null, new ImmutablePen(gradient.ToImmutable(), 2), highlightGeometry);
         }
     }
 }

+ 0 - 1
samples/Sandbox/MainWindow.axaml

@@ -1,5 +1,4 @@
 <Window xmlns="https://github.com/avaloniaui"
         xmlns:x='http://schemas.microsoft.com/winfx/2006/xaml'
         x:Class="Sandbox.MainWindow">
-    <TextBox />
 </Window>

+ 102 - 4
src/Avalonia.Base/Media/FormattedText.cs

@@ -1,8 +1,10 @@
 using System;
 using System.Collections;
+using System.Collections.Generic;
 using System.ComponentModel;
 using System.Diagnostics;
 using System.Globalization;
+using Avalonia.Controls;
 using Avalonia.Media.TextFormatting;
 using Avalonia.Utilities;
 
@@ -654,14 +656,16 @@ namespace Avalonia.Media
 
             // line break before _currentLine, needed in case we have to reformat it with collapsing symbol
             private TextLineBreak? _previousLineBreak;
+            private int _position;
+            private int _length;
 
             internal LineEnumerator(FormattedText text)
             {
                 _previousHeight = 0;
-                Length = 0;
+                _length = 0;
                 _previousLineBreak = null;
 
-                Position = 0;
+                _position = 0;
                 _lineCount = 0;
                 _totalHeight = 0;
                 Current = null;
@@ -678,9 +682,17 @@ namespace Avalonia.Media
                 _nextLine = null;
             }
 
-            private int Position { get; set; }
+            public int Position 
+            { 
+                get => _position; 
+                private set => _position = value;
+            }
 
-            private int Length { get; set; }
+            public int Length 
+            { 
+                get => _length; 
+                private set => _length = value; 
+            }
 
             /// <summary>
             /// Gets the current text line in the collection
@@ -1292,6 +1304,92 @@ namespace Avalonia.Media
             return accumulatedGeometry;
         }
 
+        /// <summary>
+        /// Builds a highlight geometry object.
+        /// </summary>
+        /// <param name="origin">The origin of the highlight region</param>
+        /// <returns>Geometry that surrounds the text.</returns>
+        public Geometry? BuildHighlightGeometry(Point origin)
+        {
+            return BuildHighlightGeometry(origin, 0, _text.Length);
+        }
+
+        /// <summary>
+        /// Builds a highlight geometry object for a given character range.
+        /// </summary>
+        /// <param name="origin">The origin of the highlight region.</param>
+        /// <param name="startIndex">The start index of initial character the bounds should be obtained for.</param>
+        /// <param name="count">The number of characters the bounds should be obtained for.</param>
+        /// <returns>Geometry that surrounds the specified character range.</returns>
+        public Geometry? BuildHighlightGeometry(Point origin, int startIndex, int count)
+        {
+            ValidateRange(startIndex, count);
+
+            Geometry? accumulatedBounds = null;
+
+            using (var enumerator = GetEnumerator())
+            {
+                var lineOrigin = origin;
+
+                while (enumerator.MoveNext())
+                {
+                    var currentLine = enumerator.Current!;
+
+                    int x0 = Math.Max(enumerator.Position, startIndex);
+                    int x1 = Math.Min(enumerator.Position + enumerator.Length, startIndex + count);
+
+                    // check if this line is intersects with the specified character range
+                    if (x0 < x1)
+                    {
+                        var highlightBounds = currentLine.GetTextBounds(x0,x1 - x0);
+
+                        if (highlightBounds != null)
+                        {
+                            foreach (var bound in highlightBounds)
+                            {
+                                var rect = bound.Rectangle;
+
+                                if (FlowDirection == FlowDirection.RightToLeft)
+                                {
+                                    // Convert logical units (which extend leftward from the right edge
+                                    // of the paragraph) to physical units.
+                                    //
+                                    // Note that since rect is in logical units, rect.Right corresponds to 
+                                    // the visual *left* edge of the rectangle in the RTL case. Specifically,
+                                    // is the distance leftward from the right edge of the formatting rectangle
+                                    // whose width is the paragraph width passed to FormatLine.
+                                    //
+                                    rect = rect.WithX(enumerator.CurrentParagraphWidth - rect.Right);
+                                }
+
+                                rect = new Rect(new Point(rect.X + lineOrigin.X, rect.Y + lineOrigin.Y), rect.Size);
+
+                                RectangleGeometry rectangleGeometry = new RectangleGeometry(rect);
+
+                                if (accumulatedBounds == null)
+                                {
+                                    accumulatedBounds = rectangleGeometry;
+                                }
+                                else
+                                {
+                                    accumulatedBounds = Geometry.Combine(accumulatedBounds, rectangleGeometry, GeometryCombineMode.Union);
+                                }                                  
+                            }
+                        }
+                    }
+
+                    AdvanceLineOrigin(ref lineOrigin, currentLine);
+                }
+            }
+
+            if (accumulatedBounds?.PlatformImpl == null || accumulatedBounds.PlatformImpl.Bounds.IsEmpty)
+            {
+                return null;
+            }            
+
+            return accumulatedBounds;
+        }
+
         /// <summary>
         /// Draws the text object
         /// </summary>

+ 13 - 0
src/Avalonia.Base/Media/Geometry.cs

@@ -185,5 +185,18 @@ namespace Avalonia.Media
             var control = e.Sender as Geometry;
             control?.InvalidateGeometry();
         }
+
+        /// <summary>
+        /// Combines the two geometries using the specified <see cref="GeometryCombineMode"/> and applies the specified transform to the resulting geometry.
+        /// </summary>
+        /// <param name="geometry1">The first geometry to combine.</param>
+        /// <param name="geometry2">The second geometry to combine.</param>
+        /// <param name="combineMode">One of the enumeration values that specifies how the geometries are combined.</param>
+        /// <param name="transform">A transformation to apply to the combined geometry, or <c>null</c>.</param>
+        /// <returns></returns>
+        public static Geometry Combine(Geometry geometry1, RectangleGeometry geometry2, GeometryCombineMode combineMode, Transform? transform = null)
+        {
+            return new CombinedGeometry(combineMode, geometry1, geometry2, transform);
+        }
     }
 }

+ 6 - 5
src/Avalonia.Base/Media/GlyphRun.cs

@@ -789,14 +789,15 @@ namespace Avalonia.Media
 
                     var clusterLength = 1;
 
-                    while (i - 1 >= 0)
+                    var j = i;
+
+                    while (j - 1 >= 0)
                     {
-                        var nextCluster = GlyphClusters[i - 1];
+                        var nextCluster = GlyphClusters[--j];
 
                         if (currentCluster == nextCluster)
                         {
-                            clusterLength++;
-                            i--;
+                            clusterLength++;                        
 
                             continue;
                         }
@@ -811,7 +812,7 @@ namespace Avalonia.Media
 
                     trailingWhitespaceLength += clusterLength;
 
-                    glyphCount++;
+                    glyphCount += clusterLength;
                 }
             }
 

+ 117 - 136
src/Avalonia.Base/Media/TextFormatting/TextLineImpl.cs

@@ -128,7 +128,7 @@ namespace Avalonia.Media.TextFormatting
 
             var collapsingProperties = collapsingPropertiesList[0];
 
-            if(collapsingProperties is null)
+            if (collapsingProperties is null)
             {
                 return this;
             }
@@ -192,7 +192,7 @@ namespace Avalonia.Media.TextFormatting
             {
                 var currentRun = _textRuns[i];
 
-                if(currentRun is ShapedTextCharacters shapedRun && !shapedRun.ShapedBuffer.IsLeftToRight)
+                if (currentRun is ShapedTextCharacters shapedRun && !shapedRun.ShapedBuffer.IsLeftToRight)
                 {
                     var rightToLeftIndex = i;
                     currentPosition += currentRun.TextSourceLength;
@@ -213,14 +213,14 @@ namespace Avalonia.Media.TextFormatting
 
                     for (var j = i; i <= rightToLeftIndex; j++)
                     {
-                        if(j > _textRuns.Count - 1)
+                        if (j > _textRuns.Count - 1)
                         {
                             break;
                         }
 
                         currentRun = _textRuns[j];
 
-                        if(currentDistance + currentRun.Size.Width <= distance)
+                        if (currentDistance + currentRun.Size.Width <= distance)
                         {
                             currentDistance += currentRun.Size.Width;
                             currentPosition -= currentRun.TextSourceLength;
@@ -266,10 +266,6 @@ namespace Avalonia.Media.TextFormatting
                         {
                             offset = Math.Max(0, currentPosition - shapedRun.Text.Start);
                         }
-                        //else
-                        //{
-                        //    offset = Math.Max(0, currentPosition - shapedRun.Text.Start + shapedRun.Text.Length);
-                        //}
 
                         characterHit = new CharacterHit(characterHit.FirstCharacterIndex + offset, characterHit.TrailingLength);
 
@@ -326,11 +322,11 @@ namespace Avalonia.Media.TextFormatting
 
                                 continue;
                             }
-                            
+
                             break;
                         }
 
-                        if(i > index)
+                        if (i > index)
                         {
                             while (i >= index)
                             {
@@ -354,7 +350,7 @@ namespace Avalonia.Media.TextFormatting
                         }
                     }
 
-                    if (currentPosition + currentRun.TextSourceLength >= characterIndex && 
+                    if (currentPosition + currentRun.TextSourceLength >= characterIndex &&
                         TryGetDistanceFromCharacterHit(currentRun, characterHit, currentPosition, remainingLength, flowDirection, out var distance, out _))
                     {
                         return Math.Max(0, currentDistance + distance);
@@ -534,6 +530,8 @@ namespace Avalonia.Media.TextFormatting
             double currentWidth = 0;
             var currentRect = Rect.Empty;
 
+            TextRunBounds lastRunBounds = default;
+
             for (var index = 0; index < TextRuns.Count; index++)
             {
                 if (TextRuns[index] is not DrawableTextRun currentRun)
@@ -543,53 +541,93 @@ namespace Avalonia.Media.TextFormatting
 
                 var characterLength = 0;
                 var endX = startX;
-                var runWidth = 0.0;
-                TextRunBounds? currentRunBounds = null;
 
                 var currentShapedRun = currentRun as ShapedTextCharacters;
 
+                TextRunBounds currentRunBounds;
+
+                double combinedWidth;
+
+                if (currentPosition + currentRun.TextSourceLength <= firstTextSourceIndex)
+                {
+                    startX += currentRun.Size.Width;
+
+                    currentPosition += currentRun.TextSourceLength;
+
+                    continue;
+                }
+
                 if (currentShapedRun != null && !currentShapedRun.ShapedBuffer.IsLeftToRight)
                 {
                     var rightToLeftIndex = index;
-                    startX += currentShapedRun.Size.Width;
+                    var rightToLeftWidth = currentShapedRun.Size.Width;
 
-                    while (rightToLeftIndex + 1 <= _textRuns.Count - 1)
+                    while (rightToLeftIndex + 1 <= _textRuns.Count - 1 && _textRuns[rightToLeftIndex + 1] is ShapedTextCharacters nextShapedRun)
                     {
-                        var nextShapedRun = _textRuns[rightToLeftIndex + 1] as ShapedTextCharacters;
-
                         if (nextShapedRun == null || nextShapedRun.ShapedBuffer.IsLeftToRight)
                         {
                             break;
                         }
 
-                        startX += nextShapedRun.Size.Width;
-
                         rightToLeftIndex++;
+
+                        rightToLeftWidth += nextShapedRun.Size.Width;
+
+                        if (currentPosition + nextShapedRun.TextSourceLength > firstTextSourceIndex + textLength)
+                        {
+                            break;
+                        }
+
+                        currentShapedRun = nextShapedRun;
                     }
 
-                    if (TryGetTextRunBoundsRightToLeft(startX, firstTextSourceIndex, characterIndex, rightToLeftIndex, ref currentPosition, ref remainingLength, out currentRunBounds))
+                    startX = startX + rightToLeftWidth;
+
+                    currentRunBounds = GetRightToLeftTextRunBounds(currentShapedRun, startX, firstTextSourceIndex, characterIndex, currentPosition, remainingLength);
+
+                    remainingLength -= currentRunBounds.Length;
+                    currentPosition = currentRunBounds.TextSourceCharacterIndex + currentRunBounds.Length;
+                    endX = currentRunBounds.Rectangle.Right;
+                    startX = currentRunBounds.Rectangle.Left;
+
+                    var rightToLeftRunBounds = new List<TextRunBounds> { currentRunBounds };
+
+                    for (int i = rightToLeftIndex - 1; i >= index; i--)
                     {
-                        startX = currentRunBounds!.Rectangle.Left;
-                        endX = currentRunBounds.Rectangle.Right;
+                        currentShapedRun = TextRuns[i] as ShapedTextCharacters;
+
+                        if(currentShapedRun == null)
+                        {
+                            continue;
+                        }
 
-                        runWidth = currentRunBounds.Rectangle.Width;
+                        currentRunBounds = GetRightToLeftTextRunBounds(currentShapedRun, startX, firstTextSourceIndex, characterIndex, currentPosition, remainingLength);
+
+                        rightToLeftRunBounds.Insert(0, currentRunBounds);
+
+                        remainingLength -= currentRunBounds.Length;
+                        startX = currentRunBounds.Rectangle.Left;
+
+                        currentPosition += currentRunBounds.Length;
                     }
 
+                    combinedWidth = endX - startX;
+
+                    currentRect = new Rect(startX, 0, combinedWidth, Height);
+
                     currentDirection = FlowDirection.RightToLeft;
+
+                    if (!MathUtilities.IsZero(combinedWidth))
+                    {
+                        result.Add(new TextBounds(currentRect, currentDirection, rightToLeftRunBounds));
+                    }
+
+                    startX = endX;
                 }
                 else
                 {
                     if (currentShapedRun != null)
                     {
-                        if (currentPosition + currentRun.TextSourceLength <= firstTextSourceIndex)
-                        {
-                            startX += currentRun.Size.Width;
-
-                            currentPosition += currentRun.TextSourceLength;
-
-                            continue;
-                        }
-
                         var offset = Math.Max(0, firstTextSourceIndex - currentPosition);
 
                         currentPosition += offset;
@@ -665,43 +703,46 @@ namespace Avalonia.Media.TextFormatting
                         characterLength = NewLineLength;
                     }
 
-                    runWidth = endX - startX;
-                    currentRunBounds = new TextRunBounds(new Rect(startX, 0, runWidth, Height), currentPosition, characterLength, currentRun);
+                    combinedWidth = endX - startX;
+
+                    currentRunBounds = new TextRunBounds(new Rect(startX, 0, combinedWidth, Height), currentPosition, characterLength, currentRun);
 
                     currentPosition += characterLength;
 
                     remainingLength -= characterLength;
-                }                 
 
-                if (currentRunBounds != null && !MathUtilities.IsZero(runWidth) || NewLineLength > 0)
-                {
-                    if (lastDirection == currentDirection && result.Count > 0 && MathUtilities.AreClose(currentRect.Right, startX))
+                    startX = endX;
+
+                    if (currentRunBounds.TextRun != null && !MathUtilities.IsZero(combinedWidth) || NewLineLength > 0)
                     {
-                        currentRect = currentRect.WithWidth(currentWidth + runWidth);
+                        if (result.Count > 0 && lastDirection == currentDirection && MathUtilities.AreClose(currentRect.Left, lastRunBounds.Rectangle.Right))
+                        {
+                            currentRect = currentRect.WithWidth(currentWidth + combinedWidth);
 
-                        var textBounds = result[result.Count - 1];
+                            var textBounds = result[result.Count - 1];
 
-                        textBounds.Rectangle = currentRect;
+                            textBounds.Rectangle = currentRect;
 
-                        textBounds.TextRunBounds.Add(currentRunBounds!);
-                    }
-                    else
-                    {
-                        currentRect = currentRunBounds!.Rectangle;
+                            textBounds.TextRunBounds.Add(currentRunBounds);
+                        }
+                        else
+                        {
+                            currentRect = currentRunBounds.Rectangle;
 
-                        result.Add(new TextBounds(currentRect, currentDirection, new List<TextRunBounds> { currentRunBounds }));
+                            result.Add(new TextBounds(currentRect, currentDirection, new List<TextRunBounds> { currentRunBounds }));
+                        }
                     }
+
+                    lastRunBounds = currentRunBounds;
                 }
 
-                currentWidth += runWidth;
-              
+                currentWidth += combinedWidth;
 
                 if (remainingLength <= 0 || currentPosition >= characterIndex)
                 {
                     break;
                 }
 
-                startX = endX;
                 lastDirection = currentDirection;
             }
 
@@ -856,105 +897,45 @@ namespace Avalonia.Media.TextFormatting
             return result;
         }
 
-        private bool TryGetTextRunBoundsRightToLeft(double startX, int firstTextSourceIndex, int characterIndex, int runIndex, ref int currentPosition, ref int remainingLength, out TextRunBounds? textRunBounds)
+        private TextRunBounds GetRightToLeftTextRunBounds(ShapedTextCharacters currentRun, double endX, int firstTextSourceIndex, int characterIndex, int currentPosition, int remainingLength)
         {
-            textRunBounds = null;
+            var startX = endX;
 
-            for (var index = runIndex; index >= 0; index--)
-            {
-                if (TextRuns[index] is not DrawableTextRun currentRun)
-                {
-                    continue;
-                }
+            var offset = Math.Max(0, firstTextSourceIndex - currentPosition);
 
-                if (currentPosition + currentRun.TextSourceLength <= firstTextSourceIndex)
-                {
-                    startX -= currentRun.Size.Width;
+            currentPosition += offset;
 
-                    currentPosition += currentRun.TextSourceLength;
+            var startIndex = currentRun.Text.Start + offset;
 
-                    continue;
-                }
+            double startOffset;
+            double endOffset;
 
-                var characterLength = 0;
-                var endX = startX;
-
-                if (currentRun is ShapedTextCharacters currentShapedRun)
-                {
-                    var offset = Math.Max(0, firstTextSourceIndex - currentPosition);
-
-                    currentPosition += offset;
-
-                    var startIndex = currentRun.Text.Start + offset;
-                    double startOffset;
-                    double endOffset;
-
-                    if (currentShapedRun.ShapedBuffer.IsLeftToRight)
-                    {
-                        if (currentPosition < startIndex)
-                        {
-                            startOffset = endOffset = 0;
-                        }
-                        else
-                        {
-                            endOffset = currentShapedRun.GlyphRun.GetDistanceFromCharacterHit(new CharacterHit(startIndex + remainingLength));
-
-                            startOffset = currentShapedRun.GlyphRun.GetDistanceFromCharacterHit(new CharacterHit(startIndex));
-                        }
-                    }
-                    else
-                    {
-                        endOffset = currentShapedRun.GlyphRun.GetDistanceFromCharacterHit(new CharacterHit(startIndex));
-
-                        startOffset = currentShapedRun.GlyphRun.GetDistanceFromCharacterHit(new CharacterHit(startIndex + remainingLength));
-                    }
-
-                    startX -= currentRun.Size.Width - startOffset;
-                    endX -= currentRun.Size.Width - endOffset;
-
-                    var endHit = currentShapedRun.GlyphRun.GetCharacterHitFromDistance(endOffset, out _);
-                    var startHit = currentShapedRun.GlyphRun.GetCharacterHitFromDistance(startOffset, out _);
+            endOffset = currentRun.GlyphRun.GetDistanceFromCharacterHit(new CharacterHit(startIndex));
 
-                    characterLength = Math.Abs(startHit.FirstCharacterIndex + startHit.TrailingLength - endHit.FirstCharacterIndex - endHit.TrailingLength);
-                }
-                else
-                {
-                    if (currentPosition + currentRun.TextSourceLength <= characterIndex)
-                    {
-                        endX -= currentRun.Size.Width;
-                    }
+            startOffset = currentRun.GlyphRun.GetDistanceFromCharacterHit(new CharacterHit(startIndex + remainingLength));
 
-                    if (currentPosition < firstTextSourceIndex)
-                    {
-                        startX -= currentRun.Size.Width;
+            startX -= currentRun.Size.Width - startOffset;
+            endX -= currentRun.Size.Width - endOffset;
 
-                        characterLength = currentRun.TextSourceLength;
-                    }
-                }
+            var endHit = currentRun.GlyphRun.GetCharacterHitFromDistance(endOffset, out _);
+            var startHit = currentRun.GlyphRun.GetCharacterHitFromDistance(startOffset, out _);
 
-                if (endX < startX)
-                {
-                    (endX, startX) = (startX, endX);
-                }
+            var characterLength = Math.Abs(startHit.FirstCharacterIndex + startHit.TrailingLength - endHit.FirstCharacterIndex - endHit.TrailingLength);
 
-                //Lines that only contain a linebreak need to be covered here
-                if (characterLength == 0)
-                {
-                    characterLength = NewLineLength;
-                }
-
-                var runWidth = endX - startX;
-
-                remainingLength -= characterLength;
-
-                currentPosition += characterLength;
-
-                textRunBounds = new TextRunBounds(new Rect(Start + startX, 0, runWidth, Height), currentPosition, characterLength, currentRun);
+            if (endX < startX)
+            {
+                (endX, startX) = (startX, endX);
+            }
 
-                return true;
+            //Lines that only contain a linebreak need to be covered here
+            if (characterLength == 0)
+            {
+                characterLength = NewLineLength;
             }
 
-            return false;
+            var runWidth = endX - startX;
+
+            return new TextRunBounds(new Rect(Start + startX, 0, runWidth, Height), currentPosition, characterLength, currentRun);
         }
 
         public override IReadOnlyList<TextBounds> GetTextBounds(int firstTextSourceIndex, int textLength)
@@ -1536,7 +1517,7 @@ namespace Avalonia.Media.TextFormatting
             var textAlignment = _paragraphProperties.TextAlignment;
             var paragraphFlowDirection = _paragraphProperties.FlowDirection;
 
-            if(textAlignment == TextAlignment.Justify)
+            if (textAlignment == TextAlignment.Justify)
             {
                 textAlignment = TextAlignment.Start;
             }

+ 1 - 1
src/Avalonia.Base/Media/TextFormatting/TextRunBounds.cs

@@ -3,7 +3,7 @@
     /// <summary>
     /// The bounding rectangle of text run
     /// </summary>
-    public sealed class TextRunBounds
+    public readonly struct TextRunBounds
     {
         /// <summary>
         /// Constructing TextRunBounds

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

@@ -105,7 +105,7 @@
       <Compile Include="..\Avalonia.Base\Metadata\NullableAttributes.cs" Link="NullableAttributes.cs" />
       <Compile Remove="../Markup/Avalonia.Markup.Xaml.Loader\xamlil.github\**\obj\**\*.cs" />
       <Compile Remove="../Markup/Avalonia.Markup.Xaml.Loader\xamlil.github\src\XamlX\IL\SreTypeSystem.cs" />
-      <PackageReference Include="Mono.Cecil" Version="0.11.2" />
+      <PackageReference Include="Mono.Cecil" Version="0.11.4" />
       <PackageReference Include="Microsoft.Build.Framework" Version="15.1.548" PrivateAssets="All" />
       <PackageReference Include="System.Numerics.Vectors" Version="4.5.0" />
     </ItemGroup>

+ 12 - 2
src/Avalonia.Controls/Documents/InlineCollection.cs

@@ -111,7 +111,7 @@ namespace Avalonia.Controls.Documents
 
         private void AddText(string text)
         {
-            if(Parent is RichTextBlock textBlock && !textBlock.HasComplexContent)
+            if (Parent is RichTextBlock textBlock && !textBlock.HasComplexContent)
             {
                 textBlock._text += text;
             }
@@ -156,7 +156,17 @@ namespace Avalonia.Controls.Documents
         {
             foreach (var child in this)
             {
-                ((ISetLogicalParent)child).SetParent(parent);
+                var oldParent = child.Parent;
+
+                if (oldParent != parent)
+                {
+                    if (oldParent != null)
+                    {
+                        ((ISetLogicalParent)child).SetParent(null);
+                    }
+
+                    ((ISetLogicalParent)child).SetParent(parent);
+                }
             }
         }
 

+ 111 - 0
src/Avalonia.Controls/Primitives/AdornerLayer.cs

@@ -27,12 +27,22 @@ namespace Avalonia.Controls.Primitives
         public static readonly AttachedProperty<bool> IsClipEnabledProperty =
             AvaloniaProperty.RegisterAttached<AdornerLayer, Visual, bool>("IsClipEnabled", true);
 
+        /// <summary>
+        /// Allows for getting and setting of the adorner for control.
+        /// </summary>
+        public static readonly AttachedProperty<Control?> AdornerProperty =
+            AvaloniaProperty.RegisterAttached<AdornerLayer, Visual, Control?>("Adorner");
+
         private static readonly AttachedProperty<AdornedElementInfo> s_adornedElementInfoProperty =
             AvaloniaProperty.RegisterAttached<AdornerLayer, Visual, AdornedElementInfo>("AdornedElementInfo");
 
+        private static readonly AttachedProperty<AdornerLayer?> s_savedAdornerLayerProperty =
+            AvaloniaProperty.RegisterAttached<Visual, Visual, AdornerLayer?>("SavedAdornerLayer");
+
         static AdornerLayer()
         {
             AdornedElementProperty.Changed.Subscribe(AdornedElementChanged);
+            AdornerProperty.Changed.Subscribe(AdornerChanged);
         }
 
         public AdornerLayer()
@@ -65,6 +75,107 @@ namespace Avalonia.Controls.Primitives
             adorner.SetValue(IsClipEnabledProperty, isClipEnabled);
         }
 
+        public static Control? GetAdorner(Visual visual)
+        {
+            return visual.GetValue(AdornerProperty);
+        }
+
+        public static void SetAdorner(Visual visual, Control? adorner)
+        {
+            visual.SetValue(AdornerProperty, adorner);
+        }
+
+        private static void AdornerChanged(AvaloniaPropertyChangedEventArgs<Control?> e)
+        {
+            if (e.Sender is Visual visual)
+            {
+                var oldAdorner = e.OldValue.GetValueOrDefault();
+                var newAdorner = e.NewValue.GetValueOrDefault();
+
+                if (Equals(oldAdorner, newAdorner))
+                {
+                    return;
+                }
+
+                if (oldAdorner is { })
+                {
+                    visual.AttachedToVisualTree -= VisualOnAttachedToVisualTree;
+                    visual.DetachedFromVisualTree -= VisualOnDetachedFromVisualTree;
+                    Detach(visual, oldAdorner);
+                }
+
+                if (newAdorner is { })
+                {
+                    visual.AttachedToVisualTree += VisualOnAttachedToVisualTree;
+                    visual.DetachedFromVisualTree += VisualOnDetachedFromVisualTree;
+                    Attach(visual, newAdorner);
+                }
+            }
+        }
+
+        private static void VisualOnAttachedToVisualTree(object? sender, VisualTreeAttachmentEventArgs e)
+        {
+            if (sender is Visual visual)
+            {
+                var adorner = GetAdorner(visual);
+                if (adorner is { })
+                {
+                    Attach(visual, adorner);
+                }
+            }
+        }
+
+        private static void VisualOnDetachedFromVisualTree(object? sender, VisualTreeAttachmentEventArgs e)
+        {
+            if (sender is Visual visual)
+            {
+                var adorner = GetAdorner(visual);
+                if (adorner is { })
+                {
+                    Detach(visual, adorner);
+                }
+            }
+        }
+
+        private static void Attach(Visual visual, Control adorner)
+        {
+            var layer = AdornerLayer.GetAdornerLayer(visual);
+            AddVisualAdorner(visual, adorner, layer);
+            visual.SetValue(s_savedAdornerLayerProperty, layer);
+        }
+
+        private static void Detach(Visual visual, Control adorner)
+        {
+            var layer = visual.GetValue(s_savedAdornerLayerProperty);
+            RemoveVisualAdorner(visual, adorner, layer);
+            visual.ClearValue(s_savedAdornerLayerProperty);
+        }
+
+        private static void AddVisualAdorner(Visual visual, Control? adorner, AdornerLayer? layer)
+        {
+            if (adorner is null || layer == null || layer.Children.Contains(adorner))
+            {
+                return;
+            }
+
+            AdornerLayer.SetAdornedElement(adorner, visual);
+            AdornerLayer.SetIsClipEnabled(adorner, false);
+
+            ((ISetLogicalParent) adorner).SetParent(visual);
+            layer.Children.Add(adorner);
+        }
+
+        private static void RemoveVisualAdorner(Visual visual, Control? adorner, AdornerLayer? layer)
+        {
+            if (adorner is null || layer is null || !layer.Children.Contains(adorner))
+            {
+                return;
+            }
+
+            layer.Children.Remove(adorner);
+            ((ISetLogicalParent) adorner).SetParent(null);
+        }
+
         protected override Size MeasureOverride(Size availableSize)
         {
             foreach (var child in Children)

+ 14 - 20
src/Avalonia.Controls/Window.cs

@@ -668,7 +668,7 @@ namespace Avalonia.Controls
             Owner = parent;
             parent?.AddChild(this, false);
 
-            SetWindowStartupLocation(Owner?.PlatformImpl);
+            SetWindowStartupLocation(parent?.PlatformImpl);
 
             PlatformImpl?.Show(ShowActivated, false);
             Renderer?.Start();
@@ -830,10 +830,11 @@ namespace Avalonia.Controls
             var startupLocation = WindowStartupLocation;
 
             if (startupLocation == WindowStartupLocation.CenterOwner &&
-                Owner is Window ownerWindow &&
-                ownerWindow.WindowState == WindowState.Minimized)
+                (owner is null || 
+                 (Owner is Window ownerWindow && ownerWindow.WindowState == WindowState.Minimized))
+                )
             {
-                // If startup location is CenterOwner, but owner is minimized then fall back
+                // If startup location is CenterOwner, but owner is null or minimized then fall back
                 // to CenterScreen. This behavior is consistent with WPF.
                 startupLocation = WindowStartupLocation.CenterScreen;
             }
@@ -851,31 +852,24 @@ namespace Avalonia.Controls
 
                 if (owner is not null)
                 {
-                    screen = Screens.ScreenFromWindow(owner);
-
-                    screen ??= Screens.ScreenFromPoint(owner.Position);
+                    screen = Screens.ScreenFromWindow(owner) 
+                             ?? Screens.ScreenFromPoint(owner.Position);
                 }
 
-                if (screen is null)
-                {
-                    screen = Screens.ScreenFromPoint(Position);
-                }
+                screen ??= Screens.ScreenFromPoint(Position);
 
-                if (screen != null)
+                if (screen is not null)
                 {
                     Position = screen.WorkingArea.CenterRect(rect).Position;
                 }
             }
             else if (startupLocation == WindowStartupLocation.CenterOwner)
             {
-                if (owner != null)
-                {
-                    var ownerSize = owner.FrameSize ?? owner.ClientSize;
-                    var ownerRect = new PixelRect(
-                        owner.Position,
-                        PixelSize.FromSize(ownerSize, scaling));
-                    Position = ownerRect.CenterRect(rect).Position;
-                }
+                var ownerSize = owner!.FrameSize ?? owner.ClientSize;
+                var ownerRect = new PixelRect(
+                    owner.Position,
+                    PixelSize.FromSize(ownerSize, scaling));
+                Position = ownerRect.CenterRect(rect).Position;
             }
         }
 

+ 1 - 1
src/Avalonia.X11/X11Window.cs

@@ -302,7 +302,7 @@ namespace Avalonia.X11
                 min_height = min.Height
             };
             hints.height_inc = hints.width_inc = 1;
-            var flags = XSizeHintsFlags.PMinSize | XSizeHintsFlags.PResizeInc;
+            var flags = XSizeHintsFlags.PMinSize | XSizeHintsFlags.PResizeInc | XSizeHintsFlags.PPosition | XSizeHintsFlags.PSize;
             // People might be passing double.MaxValue
             if (max.Width < 100000 && max.Height < 100000)
             {

+ 72 - 11
tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextLineTests.cs

@@ -597,21 +597,82 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting
 
                 textBounds = textLine.GetTextBounds(0, 20);
 
-                Assert.Equal(1, textBounds.Count);
+                Assert.Equal(2, textBounds.Count);
 
-                Assert.Equal(144.0234375, textBounds[0].Rectangle.Width);
+                Assert.Equal(144.0234375, textBounds.Sum(x => x.Rectangle.Width));
 
                 textBounds = textLine.GetTextBounds(0, 30);
 
-                Assert.Equal(1, textBounds.Count);
+                Assert.Equal(3, textBounds.Count);
 
-                Assert.Equal(216.03515625, textBounds[0].Rectangle.Width);
+                Assert.Equal(216.03515625, textBounds.Sum(x => x.Rectangle.Width));
 
                 textBounds = textLine.GetTextBounds(0, 40);
 
-                Assert.Equal(1, textBounds.Count);
+                Assert.Equal(4, textBounds.Count);
+
+                Assert.Equal(textLine.WidthIncludingTrailingWhitespace, textBounds.Sum(x => x.Rectangle.Width));
+            }
+        }
+
+        [Fact]
+        public void Should_GetTextRange()
+        {
+            var text = "שדגככעיחדגכAישדגשדגחייטYDASYWIWחיחלדשSAטויליHUHIUHUIDWKLאא'ק'קחליק/'וקןגגגלךשף'/קפוכדגכשדגשיח'/קטאגשד";
+
+            using (Start())
+            {
+                var defaultProperties = new GenericTextRunProperties(Typeface.Default);
+
+                var textSource = new SingleBufferTextSource(text, defaultProperties);
+
+                var formatter = new TextFormatterImpl();
+
+                var textLine =
+                    formatter.FormatLine(textSource, 0, double.PositiveInfinity,
+                        new GenericTextParagraphProperties(defaultProperties));
+
+                var textRuns = textLine.TextRuns.Cast<ShapedTextCharacters>().ToList();
 
-                Assert.Equal(textLine.WidthIncludingTrailingWhitespace, textBounds[0].Rectangle.Width);
+                var lineWidth = textLine.WidthIncludingTrailingWhitespace;
+
+                var textBounds = textLine.GetTextBounds(0, text.Length);
+
+                TextBounds lastBounds = null;
+
+                var runBounds = textBounds.SelectMany(x => x.TextRunBounds).ToList();
+
+                Assert.Equal(textRuns.Count, runBounds.Count);
+
+                for (var i = 0; i < textRuns.Count; i++)
+                {
+                    var run = textRuns[i];
+                    var bounds = runBounds[i];
+
+                    Assert.Equal(run.Text.Start, bounds.TextSourceCharacterIndex);
+                    Assert.Equal(run, bounds.TextRun);
+                    Assert.Equal(run.Size.Width, bounds.Rectangle.Width);
+                }
+
+                for (var i = 0; i < textBounds.Count; i++)
+                {
+                    var currentBounds = textBounds[i];
+
+                    if (lastBounds != null)
+                    {
+                        Assert.Equal(lastBounds.Rectangle.Right, currentBounds.Rectangle.Left);
+                    }
+
+                    var sumOfRunWidth = currentBounds.TextRunBounds.Sum(x => x.Rectangle.Width);
+
+                    Assert.Equal(sumOfRunWidth, currentBounds.Rectangle.Width);
+
+                    lastBounds = currentBounds;
+                }
+
+                var sumOfBoundsWidth = textBounds.Sum(x => x.Rectangle.Width);
+
+                Assert.Equal(lineWidth, sumOfBoundsWidth);
             }
         }
 
@@ -779,7 +840,7 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting
 
                 var textBounds = textLine.GetTextBounds(0, text.Length * 3 + 3);
 
-                Assert.Equal(1, textBounds.Count);
+                Assert.Equal(6, textBounds.Count);
                 Assert.Equal(textLine.WidthIncludingTrailingWhitespace, textBounds.Sum(x => x.Rectangle.Width));
 
                 textBounds = textLine.GetTextBounds(0, 1);
@@ -789,8 +850,8 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting
 
                 textBounds = textLine.GetTextBounds(0, firstRun.Text.Length + 1);
 
-                Assert.Equal(1, textBounds.Count);
-                Assert.Equal(firstRun.Size.Width + 14, textBounds[0].Rectangle.Width);
+                Assert.Equal(2, textBounds.Count);
+                Assert.Equal(firstRun.Size.Width + 14, textBounds.Sum(x => x.Rectangle.Width));
 
                 textBounds = textLine.GetTextBounds(1, firstRun.Text.Length);
 
@@ -799,8 +860,8 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting
 
                 textBounds = textLine.GetTextBounds(1, firstRun.Text.Length + 1);
 
-                Assert.Equal(1, textBounds.Count);
-                Assert.Equal(firstRun.Size.Width + 14, textBounds[0].Rectangle.Width);
+                Assert.Equal(2, textBounds.Count);
+                Assert.Equal(firstRun.Size.Width + 14, textBounds.Sum(x => x.Rectangle.Width));
             }
         }