Browse Source

Merge branch 'master' into composition-brush-3

pull/20181/head
Betta_Fish 1 week ago
committed by GitHub
parent
commit
0ec7ae2104
No known key found for this signature in database GPG Key ID: B5690EEEBB952194
  1. 181
      .github/workflows/api-diff.yml
  2. 124
      .github/workflows/update-api.yml
  3. 1
      .gitignore
  4. 3
      .gitmodules
  5. 1
      Directory.Packages.props
  6. 4
      api/Avalonia.LinuxFramebuffer.nupkg.xml
  7. 4
      api/Avalonia.Skia.nupkg.xml
  8. 412
      api/Avalonia.nupkg.xml
  9. 2
      build/SharedVersion.props
  10. 1
      external/Numerge
  11. 15
      native/Avalonia.Native/src/OSX/metal.mm
  12. 6
      nukebuild/_build.csproj
  13. 3
      samples/ControlCatalog/MainView.xaml
  14. 5
      samples/ControlCatalog/Pages/ClipboardPage.xaml.cs
  15. 14
      samples/ControlCatalog/Pages/DialogsPage.xaml.cs
  16. 50
      samples/ControlCatalog/Pages/PipsPager/PipsPagerCarouselPage.xaml
  17. 11
      samples/ControlCatalog/Pages/PipsPager/PipsPagerCarouselPage.xaml.cs
  18. 78
      samples/ControlCatalog/Pages/PipsPager/PipsPagerCustomButtonsPage.xaml
  19. 11
      samples/ControlCatalog/Pages/PipsPager/PipsPagerCustomButtonsPage.xaml.cs
  20. 59
      samples/ControlCatalog/Pages/PipsPager/PipsPagerCustomColorsPage.xaml
  21. 11
      samples/ControlCatalog/Pages/PipsPager/PipsPagerCustomColorsPage.xaml.cs
  22. 197
      samples/ControlCatalog/Pages/PipsPager/PipsPagerCustomTemplatesPage.xaml
  23. 11
      samples/ControlCatalog/Pages/PipsPager/PipsPagerCustomTemplatesPage.xaml.cs
  24. 35
      samples/ControlCatalog/Pages/PipsPager/PipsPagerEventsPage.xaml
  25. 29
      samples/ControlCatalog/Pages/PipsPager/PipsPagerEventsPage.xaml.cs
  26. 46
      samples/ControlCatalog/Pages/PipsPager/PipsPagerGettingStartedPage.xaml
  27. 11
      samples/ControlCatalog/Pages/PipsPager/PipsPagerGettingStartedPage.xaml.cs
  28. 52
      samples/ControlCatalog/Pages/PipsPager/PipsPagerLargeCollectionPage.xaml
  29. 11
      samples/ControlCatalog/Pages/PipsPager/PipsPagerLargeCollectionPage.xaml.cs
  30. 11
      samples/ControlCatalog/Pages/PipsPagerPage.xaml
  31. 47
      samples/ControlCatalog/Pages/PipsPagerPage.xaml.cs
  32. 14
      samples/SampleControls/HamburgerMenu/HamburgerMenu.xaml
  33. 4
      src/Android/Avalonia.Android/AndroidPlatform.cs
  34. 2
      src/Android/Avalonia.Android/AvaloniaView.cs
  35. 60
      src/Android/Avalonia.Android/ChoreographerTimer.cs
  36. 63
      src/Avalonia.Base/Animation/AnimationInstance`1.cs
  37. 8
      src/Avalonia.Base/Avalonia.Base.csproj
  38. 32
      src/Avalonia.Base/Compatibility/CollectionCompatibilityExtensions.cs
  39. 122
      src/Avalonia.Base/Compatibility/NativeLibrary.cs
  40. 32
      src/Avalonia.Base/Compatibility/OperatingSystem.cs
  41. 43
      src/Avalonia.Base/Compatibility/StringSyntaxAttribute.cs
  42. 2
      src/Avalonia.Base/Data/Core/ExpressionNodes/Reflection/DynamicPluginStreamNode.cs
  43. 2
      src/Avalonia.Base/Data/Core/ExpressionNodes/Reflection/ExpressionTreeIndexerNode.cs
  44. 2
      src/Avalonia.Base/Data/Core/Parsers/ExpressionNodeFactory.cs
  45. 4
      src/Avalonia.Base/Data/Core/Plugins/BindingPlugins.cs
  46. 4
      src/Avalonia.Base/Data/Core/Plugins/ReflectionMethodAccessorPlugin.cs
  47. 14
      src/Avalonia.Base/Input/DataFormat.cs
  48. 37
      src/Avalonia.Base/Input/FindNextElementOptions.cs
  49. 39
      src/Avalonia.Base/Input/FocusChangedEventArgs.cs
  50. 169
      src/Avalonia.Base/Input/FocusManager.cs
  51. 24
      src/Avalonia.Base/Input/GotFocusEventArgs.cs
  52. 63
      src/Avalonia.Base/Input/IFocusManager.cs
  53. 4
      src/Avalonia.Base/Input/IInputElement.cs
  54. 30
      src/Avalonia.Base/Input/InputElement.Gestures.cs
  55. 24
      src/Avalonia.Base/Input/InputElement.cs
  56. 4
      src/Avalonia.Base/Input/KeyGesture.cs
  57. 22
      src/Avalonia.Base/Input/KeyboardDevice.cs
  58. 29
      src/Avalonia.Base/Input/Navigation/XYFocusOptions.cs
  59. 5
      src/Avalonia.Base/Layout/LayoutHelper.cs
  60. 4
      src/Avalonia.Base/Media/Fonts/FamilyNameCollection.cs
  61. 45
      src/Avalonia.Base/Media/Fonts/FontCollectionBase.cs
  62. 15
      src/Avalonia.Base/Media/Fonts/SystemFontCollection.cs
  63. 2
      src/Avalonia.Base/Media/GlyphRun.cs
  64. 66
      src/Avalonia.Base/Media/TextFormatting/FormattedTextSource.cs
  65. 15
      src/Avalonia.Base/Media/TextFormatting/FormattingBufferHelper.cs
  66. 70
      src/Avalonia.Base/Media/TextFormatting/TextRange.cs
  67. 6
      src/Avalonia.Base/Media/Typeface.cs
  68. 13
      src/Avalonia.Base/Platform/Internal/UnmanagedBlob.cs
  69. 12
      src/Avalonia.Base/Platform/StandardRuntimePlatform.cs
  70. 3
      src/Avalonia.Base/Platform/StandardRuntimePlatformServices.cs
  71. 11
      src/Avalonia.Base/Platform/Storage/FileIO/BclLauncher.cs
  72. 7
      src/Avalonia.Base/Platform/Storage/FileIO/BclStorageProvider.cs
  73. 8
      src/Avalonia.Base/Platform/Storage/FileIO/SecurityScopedStream.cs
  74. 12
      src/Avalonia.Base/Platform/Storage/FileIO/StorageBookmarkHelper.cs
  75. 2
      src/Avalonia.Base/Rendering/Composition/Compositor.cs
  76. 4
      src/Avalonia.Base/Rendering/Composition/Server/FpsCounter.cs
  77. 5
      src/Avalonia.Base/Rendering/Composition/Server/FrameTimeGraph.cs
  78. 14
      src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionTarget.cs
  79. 6
      src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionVisual/ServerCompositionVisual.Adorners.cs
  80. 8
      src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionVisual/ServerCompositionVisual.ComputedProperties.cs
  81. 9
      src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionVisual/ServerCompositionVisual.DirtyInputs.cs
  82. 6
      src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionVisual/ServerCompositionVisual.Update.cs
  83. 44
      src/Avalonia.Base/Rendering/Composition/Server/ServerCompositor.cs
  84. 2
      src/Avalonia.Base/Rendering/Composition/Server/ServerCompositorAnimations.cs
  85. 5
      src/Avalonia.Base/Rendering/Composition/Transport/BatchStream.cs
  86. 75
      src/Avalonia.Base/Rendering/Composition/Transport/BatchStreamArrayPool.cs
  87. 44
      src/Avalonia.Base/Rendering/DefaultRenderTimer.cs
  88. 16
      src/Avalonia.Base/Rendering/IRenderLoop.cs
  89. 5
      src/Avalonia.Base/Rendering/IRenderLoopTask.cs
  90. 13
      src/Avalonia.Base/Rendering/IRenderTimer.cs
  91. 140
      src/Avalonia.Base/Rendering/RenderLoop.cs
  92. 61
      src/Avalonia.Base/Rendering/SleepLoopRenderTimer.cs
  93. 65
      src/Avalonia.Base/Rendering/ThreadProxyRenderTimer.cs
  94. 3
      src/Avalonia.Base/Threading/AvaloniaSynchronizationContext.cs
  95. 156
      src/Avalonia.Base/Threading/CulturePreservingExecutionContext.cs
  96. 8
      src/Avalonia.Base/Threading/DispatcherOperation.cs
  97. 7
      src/Avalonia.Base/Threading/NonPumpingSyncContext.cs
  98. 4
      src/Avalonia.Base/Utilities/ArrayBuilder.cs
  99. 10
      src/Avalonia.Base/Utilities/AvaloniaPropertyDictionary.cs
  100. 34
      src/Avalonia.Base/Utilities/EnumHelper.cs

181
.github/workflows/api-diff.yml

@ -0,0 +1,181 @@
name: Output API Diff
on:
issue_comment:
types: [created]
permissions: {}
concurrency:
group: api-diff-${{ github.event.issue.number }}
cancel-in-progress: true
jobs:
api-diff:
name: Output API Diff
if: >-
github.event.issue.pull_request
&& contains(github.event.comment.body, '/api-diff')
&& contains(fromJSON('["OWNER","MEMBER","COLLABORATOR"]'), github.event.comment.author_association)
runs-on: ubuntu-latest
permissions:
contents: read
pull-requests: write
steps:
- name: Check maintainer permission
uses: actions/github-script@v7
with:
script: |
const { data: permLevel } = await github.rest.repos.getCollaboratorPermissionLevel({
owner: context.repo.owner,
repo: context.repo.repo,
username: context.payload.comment.user.login,
});
const allowed = ['admin', 'maintain', 'write'];
if (!allowed.includes(permLevel.permission)) {
core.setFailed(`User @${context.payload.comment.user.login} does not have write access.`);
}
- name: Add reaction to acknowledge command
uses: actions/github-script@v7
with:
script: |
await github.rest.reactions.createForIssueComment({
owner: context.repo.owner,
repo: context.repo.repo,
comment_id: context.payload.comment.id,
content: 'eyes',
});
- name: Get PR branch info
id: pr
uses: actions/github-script@v7
with:
script: |
const { data: pr } = await github.rest.pulls.get({
owner: context.repo.owner,
repo: context.repo.repo,
pull_number: context.issue.number,
});
if (pr.head.repo.full_name !== `${context.repo.owner}/${context.repo.repo}`) {
core.setFailed('Cannot run /api-diff on fork PRs — would execute untrusted code.');
return;
}
core.setOutput('ref', pr.head.ref);
core.setOutput('sha', pr.head.sha);
- name: Checkout PR branch
uses: actions/checkout@v4
with:
ref: ${{ steps.pr.outputs.sha }}
submodules: recursive
- name: Setup .NET
uses: actions/setup-dotnet@v4
with:
global-json-file: global.json
- name: Run OutputApiDiff
run: dotnet run --project ./nukebuild/_build.csproj -- OutputApiDiff
- name: Post API diff as PR comment
if: always() && steps.pr.outcome == 'success'
uses: actions/github-script@v7
with:
script: |
const fs = require('fs');
const path = require('path');
const diffDir = path.join(process.env.GITHUB_WORKSPACE, 'artifacts', 'api-diff', 'markdown');
const mergedPath = path.join(diffDir, '_diff.md');
let body;
if (fs.existsSync(mergedPath)) {
let diff = fs.readFileSync(mergedPath, 'utf8').trim();
if (!diff || diff.toLowerCase().includes('no changes')) {
body = '### API Diff\n\n✅ No public API changes detected in this PR.';
} else {
const MAX_COMMENT_LENGTH = 60000; // GitHub comment limit is 65536
const header = '### API Diff\n\n';
const footer = '\n\n---\n_Generated by `/api-diff` command._';
const budget = MAX_COMMENT_LENGTH - header.length - footer.length;
if (diff.length > budget) {
diff = diff.substring(0, budget) + '\n\n> ⚠️ Output truncated. See the [full workflow run](' +
`${process.env.GITHUB_SERVER_URL}/${context.repo.owner}/${context.repo.repo}/actions/runs/${context.runId}` +
') for complete diff.';
}
body = header + diff + footer;
}
} else {
body = '### API Diff\n\n⚠️ No diff output was produced. Check the [workflow run](' +
`${process.env.GITHUB_SERVER_URL}/${context.repo.owner}/${context.repo.repo}/actions/runs/${context.runId}` +
') for details.';
}
// Collapse into <details> if large
if (body.length > 2000) {
const inner = body;
body = '<details>\n<summary>📋 API Diff (click to expand)</summary>\n\n' + inner + '\n\n</details>';
}
// Update existing bot comment or create a new one
const marker = '<!-- api-diff-bot -->';
body = marker + '\n' + body;
const { data: comments } = await github.rest.issues.listComments({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: context.issue.number,
per_page: 100,
});
const existing = comments.find(c => c.body?.includes(marker));
if (existing) {
await github.rest.issues.updateComment({
owner: context.repo.owner,
repo: context.repo.repo,
comment_id: existing.id,
body,
});
} else {
await github.rest.issues.createComment({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: context.issue.number,
body,
});
}
- name: Add success reaction
if: success()
uses: actions/github-script@v7
with:
script: |
await github.rest.reactions.createForIssueComment({
owner: context.repo.owner,
repo: context.repo.repo,
comment_id: context.payload.comment.id,
content: 'rocket',
});
- name: Report failure
if: failure()
uses: actions/github-script@v7
with:
script: |
await github.rest.reactions.createForIssueComment({
owner: context.repo.owner,
repo: context.repo.repo,
comment_id: context.payload.comment.id,
content: '-1',
});
await github.rest.issues.createComment({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: context.issue.number,
body: `❌ \`/api-diff\` failed. [See logs](${context.serverUrl}/${context.repo.owner}/${context.repo.repo}/actions/runs/${context.runId}).`,
});

124
.github/workflows/update-api.yml

@ -0,0 +1,124 @@
name: Update API Suppressions
on:
issue_comment:
types: [created]
permissions: {}
concurrency:
group: update-api-${{ github.event.issue.number }}
cancel-in-progress: true
jobs:
update-api:
name: Update API Suppressions
if: >-
github.event.issue.pull_request
&& contains(github.event.comment.body, '/update-api')
&& contains(fromJSON('["OWNER","MEMBER","COLLABORATOR"]'), github.event.comment.author_association)
runs-on: ubuntu-latest
permissions:
contents: write
pull-requests: write
steps:
- name: Check maintainer permission
uses: actions/github-script@v7
with:
script: |
const { data: permLevel } = await github.rest.repos.getCollaboratorPermissionLevel({
owner: context.repo.owner,
repo: context.repo.repo,
username: context.payload.comment.user.login,
});
const allowed = ['admin', 'maintain', 'write'];
if (!allowed.includes(permLevel.permission)) {
core.setFailed(`User @${context.payload.comment.user.login} does not have write access.`);
}
- name: Add reaction to acknowledge command
uses: actions/github-script@v7
with:
script: |
await github.rest.reactions.createForIssueComment({
owner: context.repo.owner,
repo: context.repo.repo,
comment_id: context.payload.comment.id,
content: 'eyes',
});
- name: Get PR branch info
id: pr
uses: actions/github-script@v7
with:
script: |
const { data: pr } = await github.rest.pulls.get({
owner: context.repo.owner,
repo: context.repo.repo,
pull_number: context.issue.number,
});
if (pr.head.repo.full_name !== `${context.repo.owner}/${context.repo.repo}`) {
core.setFailed('Cannot run /update-api on fork PRs — would execute untrusted code with write permissions.');
return;
}
core.setOutput('ref', pr.head.ref);
core.setOutput('sha', pr.head.sha);
- name: Checkout PR branch
uses: actions/checkout@v4
with:
ref: ${{ steps.pr.outputs.sha }}
token: ${{ secrets.GITHUB_TOKEN }}
submodules: recursive
- name: Setup .NET
uses: actions/setup-dotnet@v4
with:
global-json-file: global.json
- name: Run ValidateApiDiff
run: dotnet run --project ./nukebuild/_build.csproj -- ValidateApiDiff --update-api-suppression true
- name: Commit and push changes
run: |
git config user.name "github-actions[bot]"
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
git add api/
if git diff --cached --quiet; then
echo "No API suppression changes to commit."
else
git commit -m "Update API suppressions"
git push origin HEAD:${{ steps.pr.outputs.ref }}
fi
- name: Add success reaction
if: success()
uses: actions/github-script@v7
with:
script: |
await github.rest.reactions.createForIssueComment({
owner: context.repo.owner,
repo: context.repo.repo,
comment_id: context.payload.comment.id,
content: 'rocket',
});
- name: Report failure
if: failure()
uses: actions/github-script@v7
with:
script: |
await github.rest.reactions.createForIssueComment({
owner: context.repo.owner,
repo: context.repo.repo,
comment_id: context.payload.comment.id,
content: '-1',
});
await github.rest.issues.createComment({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: context.issue.number,
body: `❌ \`/update-api\` failed. [See logs](${context.serverUrl}/${context.repo.owner}/${context.repo.repo}/actions/runs/${context.runId}).`,
});

1
.gitignore

@ -219,3 +219,4 @@ src/Browser/Avalonia.Browser.Blazor/wwwroot
src/Browser/Avalonia.Browser/wwwroot
api/diff
src/Browser/Avalonia.Browser/staticwebassets
.serena

3
.gitmodules

@ -1,6 +1,3 @@
[submodule "Numerge"]
path = external/Numerge
url = https://github.com/kekekeks/Numerge.git
[submodule "XamlX"]
path = external/XamlX
url = https://github.com/kekekeks/XamlX.git

1
Directory.Packages.props

@ -36,6 +36,7 @@
<PackageVersion Include="Nito.AsyncEx.Context" Version="5.1.2" />
<PackageVersion Include="NuGet.Protocol" Version="7.0.1" />
<PackageVersion Include="Nuke.Common" Version="10.1.0" />
<PackageVersion Include="Numerge" Version="1.0.0" />
<PackageVersion Include="NUnit" Version="4.4.0" />
<PackageVersion Include="NUnit3TestAdapter" Version="6.1.0" />
<PackageVersion Include="Quamotion.RemoteViewing" Version="1.1.211" />

4
api/Avalonia.LinuxFramebuffer.nupkg.xml

@ -1,4 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<?xml version="1.0" encoding="utf-8"?>
<!-- https://learn.microsoft.com/dotnet/fundamentals/package-validation/diagnostic-ids -->
<Suppressions xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema">
<Suppression>
@ -37,4 +37,4 @@
<Left>baseline/Avalonia.LinuxFramebuffer/lib/net8.0/Avalonia.LinuxFramebuffer.dll</Left>
<Right>current/Avalonia.LinuxFramebuffer/lib/net8.0/Avalonia.LinuxFramebuffer.dll</Right>
</Suppression>
</Suppressions>
</Suppressions>

4
api/Avalonia.Skia.nupkg.xml

@ -1,4 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<?xml version="1.0" encoding="utf-8"?>
<!-- https://learn.microsoft.com/dotnet/fundamentals/package-validation/diagnostic-ids -->
<Suppressions xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema">
<Suppression>
@ -169,4 +169,4 @@
<Left>baseline/Avalonia.Skia/lib/net8.0/Avalonia.Skia.dll</Left>
<Right>current/Avalonia.Skia/lib/net8.0/Avalonia.Skia.dll</Right>
</Suppression>
</Suppressions>
</Suppressions>

412
api/Avalonia.nupkg.xml

@ -1,4 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<?xml version="1.0" encoding="utf-8"?>
<!-- https://learn.microsoft.com/dotnet/fundamentals/package-validation/diagnostic-ids -->
<Suppressions xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema">
<Suppression>
@ -109,6 +109,12 @@
<Left>baseline/Avalonia/lib/net10.0/Avalonia.Base.dll</Left>
<Right>current/Avalonia/lib/net10.0/Avalonia.Base.dll</Right>
</Suppression>
<Suppression>
<DiagnosticId>CP0001</DiagnosticId>
<Target>T:Avalonia.Input.GotFocusEventArgs</Target>
<Left>baseline/Avalonia/lib/net10.0/Avalonia.Base.dll</Left>
<Right>current/Avalonia/lib/net10.0/Avalonia.Base.dll</Right>
</Suppression>
<Suppression>
<DiagnosticId>CP0001</DiagnosticId>
<Target>T:Avalonia.Input.IDataObject</Target>
@ -157,6 +163,12 @@
<Left>baseline/Avalonia/lib/net10.0/Avalonia.Base.dll</Left>
<Right>current/Avalonia/lib/net10.0/Avalonia.Base.dll</Right>
</Suppression>
<Suppression>
<DiagnosticId>CP0001</DiagnosticId>
<Target>T:Avalonia.Media.TextFormatting.TextRange</Target>
<Left>baseline/Avalonia/lib/net10.0/Avalonia.Base.dll</Left>
<Right>current/Avalonia/lib/net10.0/Avalonia.Base.dll</Right>
</Suppression>
<Suppression>
<DiagnosticId>CP0001</DiagnosticId>
<Target>T:Avalonia.Platform.IGeometryContext2</Target>
@ -397,6 +409,12 @@
<Left>baseline/Avalonia/lib/net10.0/Avalonia.Controls.dll</Left>
<Right>current/Avalonia/lib/net10.0/Avalonia.Controls.dll</Right>
</Suppression>
<Suppression>
<DiagnosticId>CP0001</DiagnosticId>
<Target>T:Avalonia.Controls.Primitives.SelectionHandleType</Target>
<Left>baseline/Avalonia/lib/net10.0/Avalonia.Controls.dll</Left>
<Right>current/Avalonia/lib/net10.0/Avalonia.Controls.dll</Right>
</Suppression>
<Suppression>
<DiagnosticId>CP0001</DiagnosticId>
<Target>T:Avalonia.Controls.Remote.RemoteServer</Target>
@ -571,6 +589,12 @@
<Left>baseline/Avalonia/lib/net8.0/Avalonia.Base.dll</Left>
<Right>current/Avalonia/lib/net8.0/Avalonia.Base.dll</Right>
</Suppression>
<Suppression>
<DiagnosticId>CP0001</DiagnosticId>
<Target>T:Avalonia.Input.GotFocusEventArgs</Target>
<Left>baseline/Avalonia/lib/net8.0/Avalonia.Base.dll</Left>
<Right>current/Avalonia/lib/net8.0/Avalonia.Base.dll</Right>
</Suppression>
<Suppression>
<DiagnosticId>CP0001</DiagnosticId>
<Target>T:Avalonia.Input.IDataObject</Target>
@ -619,6 +643,12 @@
<Left>baseline/Avalonia/lib/net8.0/Avalonia.Base.dll</Left>
<Right>current/Avalonia/lib/net8.0/Avalonia.Base.dll</Right>
</Suppression>
<Suppression>
<DiagnosticId>CP0001</DiagnosticId>
<Target>T:Avalonia.Media.TextFormatting.TextRange</Target>
<Left>baseline/Avalonia/lib/net8.0/Avalonia.Base.dll</Left>
<Right>current/Avalonia/lib/net8.0/Avalonia.Base.dll</Right>
</Suppression>
<Suppression>
<DiagnosticId>CP0001</DiagnosticId>
<Target>T:Avalonia.Platform.IGeometryContext2</Target>
@ -859,6 +889,12 @@
<Left>baseline/Avalonia/lib/net8.0/Avalonia.Controls.dll</Left>
<Right>current/Avalonia/lib/net8.0/Avalonia.Controls.dll</Right>
</Suppression>
<Suppression>
<DiagnosticId>CP0001</DiagnosticId>
<Target>T:Avalonia.Controls.Primitives.SelectionHandleType</Target>
<Left>baseline/Avalonia/lib/net8.0/Avalonia.Controls.dll</Left>
<Right>current/Avalonia/lib/net8.0/Avalonia.Controls.dll</Right>
</Suppression>
<Suppression>
<DiagnosticId>CP0001</DiagnosticId>
<Target>T:Avalonia.Controls.Remote.RemoteServer</Target>
@ -961,6 +997,18 @@
<Left>baseline/Avalonia/lib/net10.0/Avalonia.Base.dll</Left>
<Right>current/Avalonia/lib/net10.0/Avalonia.Base.dll</Right>
</Suppression>
<Suppression>
<DiagnosticId>CP0002</DiagnosticId>
<Target>F:Avalonia.Input.InputElement.GotFocusEvent</Target>
<Left>baseline/Avalonia/lib/net10.0/Avalonia.Base.dll</Left>
<Right>current/Avalonia/lib/net10.0/Avalonia.Base.dll</Right>
</Suppression>
<Suppression>
<DiagnosticId>CP0002</DiagnosticId>
<Target>F:Avalonia.Input.InputElement.LostFocusEvent</Target>
<Left>baseline/Avalonia/lib/net10.0/Avalonia.Base.dll</Left>
<Right>current/Avalonia/lib/net10.0/Avalonia.Base.dll</Right>
</Suppression>
<Suppression>
<DiagnosticId>CP0002</DiagnosticId>
<Target>F:Avalonia.Media.DrawingImage.ViewboxProperty</Target>
@ -1069,12 +1117,48 @@
<Left>baseline/Avalonia/lib/net10.0/Avalonia.Base.dll</Left>
<Right>current/Avalonia/lib/net10.0/Avalonia.Base.dll</Right>
</Suppression>
<Suppression>
<DiagnosticId>CP0002</DiagnosticId>
<Target>M:Avalonia.Input.FocusManager.#ctor(Avalonia.Input.IInputElement)</Target>
<Left>baseline/Avalonia/lib/net10.0/Avalonia.Base.dll</Left>
<Right>current/Avalonia/lib/net10.0/Avalonia.Base.dll</Right>
</Suppression>
<Suppression>
<DiagnosticId>CP0002</DiagnosticId>
<Target>M:Avalonia.Input.FocusManager.ClearFocus</Target>
<Left>baseline/Avalonia/lib/net10.0/Avalonia.Base.dll</Left>
<Right>current/Avalonia/lib/net10.0/Avalonia.Base.dll</Right>
</Suppression>
<Suppression>
<DiagnosticId>CP0002</DiagnosticId>
<Target>M:Avalonia.Input.FocusManager.ClearFocusOnElementRemoved(Avalonia.Input.IInputElement,Avalonia.Visual)</Target>
<Left>baseline/Avalonia/lib/net10.0/Avalonia.Base.dll</Left>
<Right>current/Avalonia/lib/net10.0/Avalonia.Base.dll</Right>
</Suppression>
<Suppression>
<DiagnosticId>CP0002</DiagnosticId>
<Target>M:Avalonia.Input.FocusManager.FindNextElement(Avalonia.Input.NavigationDirection)</Target>
<Left>baseline/Avalonia/lib/net10.0/Avalonia.Base.dll</Left>
<Right>current/Avalonia/lib/net10.0/Avalonia.Base.dll</Right>
</Suppression>
<Suppression>
<DiagnosticId>CP0002</DiagnosticId>
<Target>M:Avalonia.Input.FocusManager.TryMoveFocus(Avalonia.Input.NavigationDirection)</Target>
<Left>baseline/Avalonia/lib/net10.0/Avalonia.Base.dll</Left>
<Right>current/Avalonia/lib/net10.0/Avalonia.Base.dll</Right>
</Suppression>
<Suppression>
<DiagnosticId>CP0002</DiagnosticId>
<Target>M:Avalonia.Input.HoldingRoutedEventArgs.#ctor(Avalonia.Input.HoldingState,Avalonia.Point,Avalonia.Input.PointerType)</Target>
<Left>baseline/Avalonia/lib/net10.0/Avalonia.Base.dll</Left>
<Right>current/Avalonia/lib/net10.0/Avalonia.Base.dll</Right>
</Suppression>
<Suppression>
<DiagnosticId>CP0002</DiagnosticId>
<Target>M:Avalonia.Input.IFocusManager.ClearFocus</Target>
<Left>baseline/Avalonia/lib/net10.0/Avalonia.Base.dll</Left>
<Right>current/Avalonia/lib/net10.0/Avalonia.Base.dll</Right>
</Suppression>
<Suppression>
<DiagnosticId>CP0002</DiagnosticId>
<Target>M:Avalonia.Input.IInputRoot.get_KeyboardNavigationHandler</Target>
@ -1177,6 +1261,18 @@
<Left>baseline/Avalonia/lib/net10.0/Avalonia.Base.dll</Left>
<Right>current/Avalonia/lib/net10.0/Avalonia.Base.dll</Right>
</Suppression>
<Suppression>
<DiagnosticId>CP0002</DiagnosticId>
<Target>M:Avalonia.Input.InputElement.OnGotFocus(Avalonia.Input.GotFocusEventArgs)</Target>
<Left>baseline/Avalonia/lib/net10.0/Avalonia.Base.dll</Left>
<Right>current/Avalonia/lib/net10.0/Avalonia.Base.dll</Right>
</Suppression>
<Suppression>
<DiagnosticId>CP0002</DiagnosticId>
<Target>M:Avalonia.Input.InputElement.OnLostFocus(Avalonia.Interactivity.RoutedEventArgs)</Target>
<Left>baseline/Avalonia/lib/net10.0/Avalonia.Base.dll</Left>
<Right>current/Avalonia/lib/net10.0/Avalonia.Base.dll</Right>
</Suppression>
<Suppression>
<DiagnosticId>CP0002</DiagnosticId>
<Target>M:Avalonia.Input.InputElement.RemovePinchEndedHandler(Avalonia.Interactivity.Interactive,System.EventHandler{Avalonia.Input.PinchEndedEventArgs})</Target>
@ -1375,6 +1471,12 @@
<Left>baseline/Avalonia/lib/net10.0/Avalonia.Base.dll</Left>
<Right>current/Avalonia/lib/net10.0/Avalonia.Base.dll</Right>
</Suppression>
<Suppression>
<DiagnosticId>CP0002</DiagnosticId>
<Target>M:Avalonia.Media.Fonts.FontCollectionBase.TryGetGlyphTypeface(System.String,Avalonia.Media.Fonts.FontCollectionKey,Avalonia.Media.GlyphTypeface@)</Target>
<Left>baseline/Avalonia/lib/net10.0/Avalonia.Base.dll</Left>
<Right>current/Avalonia/lib/net10.0/Avalonia.Base.dll</Right>
</Suppression>
<Suppression>
<DiagnosticId>CP0002</DiagnosticId>
<Target>M:Avalonia.Media.Fonts.IFontCollection.Initialize(Avalonia.Platform.IFontManagerImpl)</Target>
@ -1555,6 +1657,42 @@
<Left>baseline/Avalonia/lib/net10.0/Avalonia.Base.dll</Left>
<Right>current/Avalonia/lib/net10.0/Avalonia.Base.dll</Right>
</Suppression>
<Suppression>
<DiagnosticId>CP0002</DiagnosticId>
<Target>M:Avalonia.Rendering.DefaultRenderTimer.add_Tick(System.Action{System.TimeSpan})</Target>
<Left>baseline/Avalonia/lib/net10.0/Avalonia.Base.dll</Left>
<Right>current/Avalonia/lib/net10.0/Avalonia.Base.dll</Right>
</Suppression>
<Suppression>
<DiagnosticId>CP0002</DiagnosticId>
<Target>M:Avalonia.Rendering.DefaultRenderTimer.remove_Tick(System.Action{System.TimeSpan})</Target>
<Left>baseline/Avalonia/lib/net10.0/Avalonia.Base.dll</Left>
<Right>current/Avalonia/lib/net10.0/Avalonia.Base.dll</Right>
</Suppression>
<Suppression>
<DiagnosticId>CP0002</DiagnosticId>
<Target>M:Avalonia.Rendering.DefaultRenderTimer.Start</Target>
<Left>baseline/Avalonia/lib/net10.0/Avalonia.Base.dll</Left>
<Right>current/Avalonia/lib/net10.0/Avalonia.Base.dll</Right>
</Suppression>
<Suppression>
<DiagnosticId>CP0002</DiagnosticId>
<Target>M:Avalonia.Rendering.DefaultRenderTimer.Stop</Target>
<Left>baseline/Avalonia/lib/net10.0/Avalonia.Base.dll</Left>
<Right>current/Avalonia/lib/net10.0/Avalonia.Base.dll</Right>
</Suppression>
<Suppression>
<DiagnosticId>CP0002</DiagnosticId>
<Target>M:Avalonia.Rendering.IRenderTimer.add_Tick(System.Action{System.TimeSpan})</Target>
<Left>baseline/Avalonia/lib/net10.0/Avalonia.Base.dll</Left>
<Right>current/Avalonia/lib/net10.0/Avalonia.Base.dll</Right>
</Suppression>
<Suppression>
<DiagnosticId>CP0002</DiagnosticId>
<Target>M:Avalonia.Rendering.IRenderTimer.remove_Tick(System.Action{System.TimeSpan})</Target>
<Left>baseline/Avalonia/lib/net10.0/Avalonia.Base.dll</Left>
<Right>current/Avalonia/lib/net10.0/Avalonia.Base.dll</Right>
</Suppression>
<Suppression>
<DiagnosticId>CP0002</DiagnosticId>
<Target>M:Avalonia.Rendering.SceneInvalidatedEventArgs.#ctor(Avalonia.Rendering.IRenderRoot,Avalonia.Rect)</Target>
@ -1567,6 +1705,30 @@
<Left>baseline/Avalonia/lib/net10.0/Avalonia.Base.dll</Left>
<Right>current/Avalonia/lib/net10.0/Avalonia.Base.dll</Right>
</Suppression>
<Suppression>
<DiagnosticId>CP0002</DiagnosticId>
<Target>M:Avalonia.Rendering.SleepLoopRenderTimer.add_Tick(System.Action{System.TimeSpan})</Target>
<Left>baseline/Avalonia/lib/net10.0/Avalonia.Base.dll</Left>
<Right>current/Avalonia/lib/net10.0/Avalonia.Base.dll</Right>
</Suppression>
<Suppression>
<DiagnosticId>CP0002</DiagnosticId>
<Target>M:Avalonia.Rendering.SleepLoopRenderTimer.remove_Tick(System.Action{System.TimeSpan})</Target>
<Left>baseline/Avalonia/lib/net10.0/Avalonia.Base.dll</Left>
<Right>current/Avalonia/lib/net10.0/Avalonia.Base.dll</Right>
</Suppression>
<Suppression>
<DiagnosticId>CP0002</DiagnosticId>
<Target>M:Avalonia.Rendering.ThreadProxyRenderTimer.add_Tick(System.Action{System.TimeSpan})</Target>
<Left>baseline/Avalonia/lib/net10.0/Avalonia.Base.dll</Left>
<Right>current/Avalonia/lib/net10.0/Avalonia.Base.dll</Right>
</Suppression>
<Suppression>
<DiagnosticId>CP0002</DiagnosticId>
<Target>M:Avalonia.Rendering.ThreadProxyRenderTimer.remove_Tick(System.Action{System.TimeSpan})</Target>
<Left>baseline/Avalonia/lib/net10.0/Avalonia.Base.dll</Left>
<Right>current/Avalonia/lib/net10.0/Avalonia.Base.dll</Right>
</Suppression>
<Suppression>
<DiagnosticId>CP0002</DiagnosticId>
<Target>M:Avalonia.Utilities.AvaloniaResourcesIndexReaderWriter.WriteResources(System.IO.Stream,System.Collections.Generic.List{System.ValueTuple{System.String,System.Int32,System.Func{System.IO.Stream}}})</Target>
@ -1855,6 +2017,12 @@
<Left>baseline/Avalonia/lib/net10.0/Avalonia.Controls.dll</Left>
<Right>current/Avalonia/lib/net10.0/Avalonia.Controls.dll</Right>
</Suppression>
<Suppression>
<DiagnosticId>CP0002</DiagnosticId>
<Target>M:Avalonia.Controls.Platform.DefaultMenuInteractionHandler.GotFocus(System.Object,Avalonia.Input.GotFocusEventArgs)</Target>
<Left>baseline/Avalonia/lib/net10.0/Avalonia.Controls.dll</Left>
<Right>current/Avalonia/lib/net10.0/Avalonia.Controls.dll</Right>
</Suppression>
<Suppression>
<DiagnosticId>CP0002</DiagnosticId>
<Target>M:Avalonia.Controls.Platform.IInsetsManager.get_DisplayEdgeToEdge</Target>
@ -2359,6 +2527,18 @@
<Left>baseline/Avalonia/lib/net8.0/Avalonia.Base.dll</Left>
<Right>current/Avalonia/lib/net8.0/Avalonia.Base.dll</Right>
</Suppression>
<Suppression>
<DiagnosticId>CP0002</DiagnosticId>
<Target>F:Avalonia.Input.InputElement.GotFocusEvent</Target>
<Left>baseline/Avalonia/lib/net8.0/Avalonia.Base.dll</Left>
<Right>current/Avalonia/lib/net8.0/Avalonia.Base.dll</Right>
</Suppression>
<Suppression>
<DiagnosticId>CP0002</DiagnosticId>
<Target>F:Avalonia.Input.InputElement.LostFocusEvent</Target>
<Left>baseline/Avalonia/lib/net8.0/Avalonia.Base.dll</Left>
<Right>current/Avalonia/lib/net8.0/Avalonia.Base.dll</Right>
</Suppression>
<Suppression>
<DiagnosticId>CP0002</DiagnosticId>
<Target>F:Avalonia.Media.DrawingImage.ViewboxProperty</Target>
@ -2467,12 +2647,48 @@
<Left>baseline/Avalonia/lib/net8.0/Avalonia.Base.dll</Left>
<Right>current/Avalonia/lib/net8.0/Avalonia.Base.dll</Right>
</Suppression>
<Suppression>
<DiagnosticId>CP0002</DiagnosticId>
<Target>M:Avalonia.Input.FocusManager.#ctor(Avalonia.Input.IInputElement)</Target>
<Left>baseline/Avalonia/lib/net8.0/Avalonia.Base.dll</Left>
<Right>current/Avalonia/lib/net8.0/Avalonia.Base.dll</Right>
</Suppression>
<Suppression>
<DiagnosticId>CP0002</DiagnosticId>
<Target>M:Avalonia.Input.FocusManager.ClearFocus</Target>
<Left>baseline/Avalonia/lib/net8.0/Avalonia.Base.dll</Left>
<Right>current/Avalonia/lib/net8.0/Avalonia.Base.dll</Right>
</Suppression>
<Suppression>
<DiagnosticId>CP0002</DiagnosticId>
<Target>M:Avalonia.Input.FocusManager.ClearFocusOnElementRemoved(Avalonia.Input.IInputElement,Avalonia.Visual)</Target>
<Left>baseline/Avalonia/lib/net8.0/Avalonia.Base.dll</Left>
<Right>current/Avalonia/lib/net8.0/Avalonia.Base.dll</Right>
</Suppression>
<Suppression>
<DiagnosticId>CP0002</DiagnosticId>
<Target>M:Avalonia.Input.FocusManager.FindNextElement(Avalonia.Input.NavigationDirection)</Target>
<Left>baseline/Avalonia/lib/net8.0/Avalonia.Base.dll</Left>
<Right>current/Avalonia/lib/net8.0/Avalonia.Base.dll</Right>
</Suppression>
<Suppression>
<DiagnosticId>CP0002</DiagnosticId>
<Target>M:Avalonia.Input.FocusManager.TryMoveFocus(Avalonia.Input.NavigationDirection)</Target>
<Left>baseline/Avalonia/lib/net8.0/Avalonia.Base.dll</Left>
<Right>current/Avalonia/lib/net8.0/Avalonia.Base.dll</Right>
</Suppression>
<Suppression>
<DiagnosticId>CP0002</DiagnosticId>
<Target>M:Avalonia.Input.HoldingRoutedEventArgs.#ctor(Avalonia.Input.HoldingState,Avalonia.Point,Avalonia.Input.PointerType)</Target>
<Left>baseline/Avalonia/lib/net8.0/Avalonia.Base.dll</Left>
<Right>current/Avalonia/lib/net8.0/Avalonia.Base.dll</Right>
</Suppression>
<Suppression>
<DiagnosticId>CP0002</DiagnosticId>
<Target>M:Avalonia.Input.IFocusManager.ClearFocus</Target>
<Left>baseline/Avalonia/lib/net8.0/Avalonia.Base.dll</Left>
<Right>current/Avalonia/lib/net8.0/Avalonia.Base.dll</Right>
</Suppression>
<Suppression>
<DiagnosticId>CP0002</DiagnosticId>
<Target>M:Avalonia.Input.IInputRoot.get_KeyboardNavigationHandler</Target>
@ -2575,6 +2791,18 @@
<Left>baseline/Avalonia/lib/net8.0/Avalonia.Base.dll</Left>
<Right>current/Avalonia/lib/net8.0/Avalonia.Base.dll</Right>
</Suppression>
<Suppression>
<DiagnosticId>CP0002</DiagnosticId>
<Target>M:Avalonia.Input.InputElement.OnGotFocus(Avalonia.Input.GotFocusEventArgs)</Target>
<Left>baseline/Avalonia/lib/net8.0/Avalonia.Base.dll</Left>
<Right>current/Avalonia/lib/net8.0/Avalonia.Base.dll</Right>
</Suppression>
<Suppression>
<DiagnosticId>CP0002</DiagnosticId>
<Target>M:Avalonia.Input.InputElement.OnLostFocus(Avalonia.Interactivity.RoutedEventArgs)</Target>
<Left>baseline/Avalonia/lib/net8.0/Avalonia.Base.dll</Left>
<Right>current/Avalonia/lib/net8.0/Avalonia.Base.dll</Right>
</Suppression>
<Suppression>
<DiagnosticId>CP0002</DiagnosticId>
<Target>M:Avalonia.Input.InputElement.RemovePinchEndedHandler(Avalonia.Interactivity.Interactive,System.EventHandler{Avalonia.Input.PinchEndedEventArgs})</Target>
@ -2773,6 +3001,12 @@
<Left>baseline/Avalonia/lib/net8.0/Avalonia.Base.dll</Left>
<Right>current/Avalonia/lib/net8.0/Avalonia.Base.dll</Right>
</Suppression>
<Suppression>
<DiagnosticId>CP0002</DiagnosticId>
<Target>M:Avalonia.Media.Fonts.FontCollectionBase.TryGetGlyphTypeface(System.String,Avalonia.Media.Fonts.FontCollectionKey,Avalonia.Media.GlyphTypeface@)</Target>
<Left>baseline/Avalonia/lib/net8.0/Avalonia.Base.dll</Left>
<Right>current/Avalonia/lib/net8.0/Avalonia.Base.dll</Right>
</Suppression>
<Suppression>
<DiagnosticId>CP0002</DiagnosticId>
<Target>M:Avalonia.Media.Fonts.IFontCollection.Initialize(Avalonia.Platform.IFontManagerImpl)</Target>
@ -2953,6 +3187,42 @@
<Left>baseline/Avalonia/lib/net8.0/Avalonia.Base.dll</Left>
<Right>current/Avalonia/lib/net8.0/Avalonia.Base.dll</Right>
</Suppression>
<Suppression>
<DiagnosticId>CP0002</DiagnosticId>
<Target>M:Avalonia.Rendering.DefaultRenderTimer.add_Tick(System.Action{System.TimeSpan})</Target>
<Left>baseline/Avalonia/lib/net8.0/Avalonia.Base.dll</Left>
<Right>current/Avalonia/lib/net8.0/Avalonia.Base.dll</Right>
</Suppression>
<Suppression>
<DiagnosticId>CP0002</DiagnosticId>
<Target>M:Avalonia.Rendering.DefaultRenderTimer.remove_Tick(System.Action{System.TimeSpan})</Target>
<Left>baseline/Avalonia/lib/net8.0/Avalonia.Base.dll</Left>
<Right>current/Avalonia/lib/net8.0/Avalonia.Base.dll</Right>
</Suppression>
<Suppression>
<DiagnosticId>CP0002</DiagnosticId>
<Target>M:Avalonia.Rendering.DefaultRenderTimer.Start</Target>
<Left>baseline/Avalonia/lib/net8.0/Avalonia.Base.dll</Left>
<Right>current/Avalonia/lib/net8.0/Avalonia.Base.dll</Right>
</Suppression>
<Suppression>
<DiagnosticId>CP0002</DiagnosticId>
<Target>M:Avalonia.Rendering.DefaultRenderTimer.Stop</Target>
<Left>baseline/Avalonia/lib/net8.0/Avalonia.Base.dll</Left>
<Right>current/Avalonia/lib/net8.0/Avalonia.Base.dll</Right>
</Suppression>
<Suppression>
<DiagnosticId>CP0002</DiagnosticId>
<Target>M:Avalonia.Rendering.IRenderTimer.add_Tick(System.Action{System.TimeSpan})</Target>
<Left>baseline/Avalonia/lib/net8.0/Avalonia.Base.dll</Left>
<Right>current/Avalonia/lib/net8.0/Avalonia.Base.dll</Right>
</Suppression>
<Suppression>
<DiagnosticId>CP0002</DiagnosticId>
<Target>M:Avalonia.Rendering.IRenderTimer.remove_Tick(System.Action{System.TimeSpan})</Target>
<Left>baseline/Avalonia/lib/net8.0/Avalonia.Base.dll</Left>
<Right>current/Avalonia/lib/net8.0/Avalonia.Base.dll</Right>
</Suppression>
<Suppression>
<DiagnosticId>CP0002</DiagnosticId>
<Target>M:Avalonia.Rendering.SceneInvalidatedEventArgs.#ctor(Avalonia.Rendering.IRenderRoot,Avalonia.Rect)</Target>
@ -2965,6 +3235,30 @@
<Left>baseline/Avalonia/lib/net8.0/Avalonia.Base.dll</Left>
<Right>current/Avalonia/lib/net8.0/Avalonia.Base.dll</Right>
</Suppression>
<Suppression>
<DiagnosticId>CP0002</DiagnosticId>
<Target>M:Avalonia.Rendering.SleepLoopRenderTimer.add_Tick(System.Action{System.TimeSpan})</Target>
<Left>baseline/Avalonia/lib/net8.0/Avalonia.Base.dll</Left>
<Right>current/Avalonia/lib/net8.0/Avalonia.Base.dll</Right>
</Suppression>
<Suppression>
<DiagnosticId>CP0002</DiagnosticId>
<Target>M:Avalonia.Rendering.SleepLoopRenderTimer.remove_Tick(System.Action{System.TimeSpan})</Target>
<Left>baseline/Avalonia/lib/net8.0/Avalonia.Base.dll</Left>
<Right>current/Avalonia/lib/net8.0/Avalonia.Base.dll</Right>
</Suppression>
<Suppression>
<DiagnosticId>CP0002</DiagnosticId>
<Target>M:Avalonia.Rendering.ThreadProxyRenderTimer.add_Tick(System.Action{System.TimeSpan})</Target>
<Left>baseline/Avalonia/lib/net8.0/Avalonia.Base.dll</Left>
<Right>current/Avalonia/lib/net8.0/Avalonia.Base.dll</Right>
</Suppression>
<Suppression>
<DiagnosticId>CP0002</DiagnosticId>
<Target>M:Avalonia.Rendering.ThreadProxyRenderTimer.remove_Tick(System.Action{System.TimeSpan})</Target>
<Left>baseline/Avalonia/lib/net8.0/Avalonia.Base.dll</Left>
<Right>current/Avalonia/lib/net8.0/Avalonia.Base.dll</Right>
</Suppression>
<Suppression>
<DiagnosticId>CP0002</DiagnosticId>
<Target>M:Avalonia.Utilities.AvaloniaResourcesIndexReaderWriter.WriteResources(System.IO.Stream,System.Collections.Generic.List{System.ValueTuple{System.String,System.Int32,System.Func{System.IO.Stream}}})</Target>
@ -3253,6 +3547,12 @@
<Left>baseline/Avalonia/lib/net8.0/Avalonia.Controls.dll</Left>
<Right>current/Avalonia/lib/net8.0/Avalonia.Controls.dll</Right>
</Suppression>
<Suppression>
<DiagnosticId>CP0002</DiagnosticId>
<Target>M:Avalonia.Controls.Platform.DefaultMenuInteractionHandler.GotFocus(System.Object,Avalonia.Input.GotFocusEventArgs)</Target>
<Left>baseline/Avalonia/lib/net8.0/Avalonia.Controls.dll</Left>
<Right>current/Avalonia/lib/net8.0/Avalonia.Controls.dll</Right>
</Suppression>
<Suppression>
<DiagnosticId>CP0002</DiagnosticId>
<Target>M:Avalonia.Controls.Platform.IInsetsManager.get_DisplayEdgeToEdge</Target>
@ -3787,6 +4087,48 @@
<Left>baseline/Avalonia/lib/net8.0/Avalonia.OpenGL.dll</Left>
<Right>current/Avalonia/lib/net8.0/Avalonia.OpenGL.dll</Right>
</Suppression>
<Suppression>
<DiagnosticId>CP0006</DiagnosticId>
<Target>E:Avalonia.Input.IInputElement.GotFocus</Target>
<Left>baseline/Avalonia/lib/net10.0/Avalonia.Base.dll</Left>
<Right>current/Avalonia/lib/net10.0/Avalonia.Base.dll</Right>
</Suppression>
<Suppression>
<DiagnosticId>CP0006</DiagnosticId>
<Target>E:Avalonia.Input.IInputElement.LostFocus</Target>
<Left>baseline/Avalonia/lib/net10.0/Avalonia.Base.dll</Left>
<Right>current/Avalonia/lib/net10.0/Avalonia.Base.dll</Right>
</Suppression>
<Suppression>
<DiagnosticId>CP0006</DiagnosticId>
<Target>M:Avalonia.Input.IFocusManager.FindFirstFocusableElement</Target>
<Left>baseline/Avalonia/lib/net10.0/Avalonia.Base.dll</Left>
<Right>current/Avalonia/lib/net10.0/Avalonia.Base.dll</Right>
</Suppression>
<Suppression>
<DiagnosticId>CP0006</DiagnosticId>
<Target>M:Avalonia.Input.IFocusManager.FindLastFocusableElement</Target>
<Left>baseline/Avalonia/lib/net10.0/Avalonia.Base.dll</Left>
<Right>current/Avalonia/lib/net10.0/Avalonia.Base.dll</Right>
</Suppression>
<Suppression>
<DiagnosticId>CP0006</DiagnosticId>
<Target>M:Avalonia.Input.IFocusManager.FindNextElement(Avalonia.Input.NavigationDirection,Avalonia.Input.FindNextElementOptions)</Target>
<Left>baseline/Avalonia/lib/net10.0/Avalonia.Base.dll</Left>
<Right>current/Avalonia/lib/net10.0/Avalonia.Base.dll</Right>
</Suppression>
<Suppression>
<DiagnosticId>CP0006</DiagnosticId>
<Target>M:Avalonia.Input.IFocusManager.Focus(Avalonia.Input.IInputElement,Avalonia.Input.NavigationMethod,Avalonia.Input.KeyModifiers)</Target>
<Left>baseline/Avalonia/lib/net10.0/Avalonia.Base.dll</Left>
<Right>current/Avalonia/lib/net10.0/Avalonia.Base.dll</Right>
</Suppression>
<Suppression>
<DiagnosticId>CP0006</DiagnosticId>
<Target>M:Avalonia.Input.IFocusManager.TryMoveFocus(Avalonia.Input.NavigationDirection,Avalonia.Input.FindNextElementOptions)</Target>
<Left>baseline/Avalonia/lib/net10.0/Avalonia.Base.dll</Left>
<Right>current/Avalonia/lib/net10.0/Avalonia.Base.dll</Right>
</Suppression>
<Suppression>
<DiagnosticId>CP0006</DiagnosticId>
<Target>M:Avalonia.Input.IKeyboardNavigationHandler.Move(Avalonia.Input.IInputElement,Avalonia.Input.NavigationDirection,Avalonia.Input.KeyModifiers,System.Nullable{Avalonia.Input.KeyDeviceType})</Target>
@ -3883,6 +4225,18 @@
<Left>baseline/Avalonia/lib/net10.0/Avalonia.Base.dll</Left>
<Right>current/Avalonia/lib/net10.0/Avalonia.Base.dll</Right>
</Suppression>
<Suppression>
<DiagnosticId>CP0006</DiagnosticId>
<Target>M:Avalonia.Rendering.IRenderTimer.Start</Target>
<Left>baseline/Avalonia/lib/net10.0/Avalonia.Base.dll</Left>
<Right>current/Avalonia/lib/net10.0/Avalonia.Base.dll</Right>
</Suppression>
<Suppression>
<DiagnosticId>CP0006</DiagnosticId>
<Target>M:Avalonia.Rendering.IRenderTimer.Stop</Target>
<Left>baseline/Avalonia/lib/net10.0/Avalonia.Base.dll</Left>
<Right>current/Avalonia/lib/net10.0/Avalonia.Base.dll</Right>
</Suppression>
<Suppression>
<DiagnosticId>CP0006</DiagnosticId>
<Target>P:Avalonia.Input.IInputRoot.FocusRoot</Target>
@ -4051,6 +4405,48 @@
<Left>baseline/Avalonia/lib/net6.0/Avalonia.OpenGL.dll</Left>
<Right>current/Avalonia/lib/net6.0/Avalonia.OpenGL.dll</Right>
</Suppression>
<Suppression>
<DiagnosticId>CP0006</DiagnosticId>
<Target>E:Avalonia.Input.IInputElement.GotFocus</Target>
<Left>baseline/Avalonia/lib/net8.0/Avalonia.Base.dll</Left>
<Right>current/Avalonia/lib/net8.0/Avalonia.Base.dll</Right>
</Suppression>
<Suppression>
<DiagnosticId>CP0006</DiagnosticId>
<Target>E:Avalonia.Input.IInputElement.LostFocus</Target>
<Left>baseline/Avalonia/lib/net8.0/Avalonia.Base.dll</Left>
<Right>current/Avalonia/lib/net8.0/Avalonia.Base.dll</Right>
</Suppression>
<Suppression>
<DiagnosticId>CP0006</DiagnosticId>
<Target>M:Avalonia.Input.IFocusManager.FindFirstFocusableElement</Target>
<Left>baseline/Avalonia/lib/net8.0/Avalonia.Base.dll</Left>
<Right>current/Avalonia/lib/net8.0/Avalonia.Base.dll</Right>
</Suppression>
<Suppression>
<DiagnosticId>CP0006</DiagnosticId>
<Target>M:Avalonia.Input.IFocusManager.FindLastFocusableElement</Target>
<Left>baseline/Avalonia/lib/net8.0/Avalonia.Base.dll</Left>
<Right>current/Avalonia/lib/net8.0/Avalonia.Base.dll</Right>
</Suppression>
<Suppression>
<DiagnosticId>CP0006</DiagnosticId>
<Target>M:Avalonia.Input.IFocusManager.FindNextElement(Avalonia.Input.NavigationDirection,Avalonia.Input.FindNextElementOptions)</Target>
<Left>baseline/Avalonia/lib/net8.0/Avalonia.Base.dll</Left>
<Right>current/Avalonia/lib/net8.0/Avalonia.Base.dll</Right>
</Suppression>
<Suppression>
<DiagnosticId>CP0006</DiagnosticId>
<Target>M:Avalonia.Input.IFocusManager.Focus(Avalonia.Input.IInputElement,Avalonia.Input.NavigationMethod,Avalonia.Input.KeyModifiers)</Target>
<Left>baseline/Avalonia/lib/net8.0/Avalonia.Base.dll</Left>
<Right>current/Avalonia/lib/net8.0/Avalonia.Base.dll</Right>
</Suppression>
<Suppression>
<DiagnosticId>CP0006</DiagnosticId>
<Target>M:Avalonia.Input.IFocusManager.TryMoveFocus(Avalonia.Input.NavigationDirection,Avalonia.Input.FindNextElementOptions)</Target>
<Left>baseline/Avalonia/lib/net8.0/Avalonia.Base.dll</Left>
<Right>current/Avalonia/lib/net8.0/Avalonia.Base.dll</Right>
</Suppression>
<Suppression>
<DiagnosticId>CP0006</DiagnosticId>
<Target>M:Avalonia.Input.IKeyboardNavigationHandler.Move(Avalonia.Input.IInputElement,Avalonia.Input.NavigationDirection,Avalonia.Input.KeyModifiers,System.Nullable{Avalonia.Input.KeyDeviceType})</Target>
@ -4183,6 +4579,18 @@
<Left>baseline/Avalonia/lib/net8.0/Avalonia.Base.dll</Left>
<Right>current/Avalonia/lib/net8.0/Avalonia.Base.dll</Right>
</Suppression>
<Suppression>
<DiagnosticId>CP0006</DiagnosticId>
<Target>M:Avalonia.Rendering.IRenderTimer.Start</Target>
<Left>baseline/Avalonia/lib/net8.0/Avalonia.Base.dll</Left>
<Right>current/Avalonia/lib/net8.0/Avalonia.Base.dll</Right>
</Suppression>
<Suppression>
<DiagnosticId>CP0006</DiagnosticId>
<Target>M:Avalonia.Rendering.IRenderTimer.Stop</Target>
<Left>baseline/Avalonia/lib/net8.0/Avalonia.Base.dll</Left>
<Right>current/Avalonia/lib/net8.0/Avalonia.Base.dll</Right>
</Suppression>
<Suppression>
<DiagnosticId>CP0006</DiagnosticId>
<Target>P:Avalonia.Input.IInputRoot.FocusRoot</Target>
@ -4969,4 +5377,4 @@
<Left>baseline/Avalonia/lib/netstandard2.0/Avalonia.Base.dll</Left>
<Right>current/Avalonia/lib/netstandard2.0/Avalonia.Base.dll</Right>
</Suppression>
</Suppressions>
</Suppressions>

2
build/SharedVersion.props

@ -8,7 +8,7 @@
<PackageProjectUrl>https://avaloniaui.net/?utm_source=nuget&amp;utm_medium=referral&amp;utm_content=project_homepage_link</PackageProjectUrl>
<RepositoryUrl>https://github.com/AvaloniaUI/Avalonia/</RepositoryUrl>
<GenerateDocumentationFile>true</GenerateDocumentationFile>
<NoWarn>$(NoWarn);CS1591</NoWarn>
<NoWarn>$(NoWarn);CS1591;NU5104</NoWarn>
<PackageLicenseExpression>MIT</PackageLicenseExpression>
<PackageIcon>Icon.png</PackageIcon>
<PackageDescription>Avalonia is a cross-platform UI framework for .NET providing a flexible styling system and supporting a wide range of Operating Systems such as Windows, Linux, macOS and with experimental support for Android, iOS and WebAssembly.</PackageDescription>

1
external/Numerge

@ -1 +0,0 @@
Subproject commit 5530e1cbe9e105ff4ebc9da1f4af3253a8756754

15
native/Avalonia.Native/src/OSX/metal.mm

@ -87,11 +87,12 @@ public:
return (__bridge void*) queue;
}
HRESULT ImportIOSurface(void *handle, AvnPixelFormat pixelFormat, IAvnMetalTexture **ppv) override {
HRESULT ImportIOSurface(void *handle, AvnPixelFormat pixelFormat, IAvnMetalTexture **ppv) override {
START_COM_ARP_CALL;
auto surf = (IOSurfaceRef)handle;
auto width = IOSurfaceGetWidth(surf);
auto height = IOSurfaceGetHeight(surf);
auto desc = [MTLTextureDescriptor new];
if(pixelFormat == kAvnRgba8888)
desc.pixelFormat = MTLPixelFormatRGBA8Unorm;
@ -106,13 +107,12 @@ public:
desc.mipmapLevelCount = 1;
desc.sampleCount = 1;
desc.usage = MTLTextureUsageShaderRead | MTLTextureUsageRenderTarget;
auto texture = [device newTextureWithDescriptor:desc iosurface:surf plane:0];
if(texture == nullptr)
return E_FAIL;
*ppv = new AvnMetalTexture(texture);
return S_OK;
}
HRESULT ImportSharedEvent(void *mtlSharedEventInstance, IAvnMTLSharedEvent**ppv) override {
@ -132,11 +132,12 @@ public:
HRESULT SignalOrWait(IAvnMTLSharedEvent *ev, uint64_t value, bool wait)
{
START_ARP_CALL;
if (@available(macOS 12.0, *))
{
auto e = dynamic_cast<AvnMTLSharedEvent*>(ev);
if(e == nullptr)
return E_FAIL;;
return E_FAIL;
auto buf = [queue commandBuffer];
if(wait)
[buf encodeWaitForEvent:e->GetEvent() value:value];
@ -204,6 +205,7 @@ public:
~AvnMetalRenderSession()
{
START_ARP_CALL;
auto buffer = [_queue commandBuffer];
[buffer presentDrawable: _drawable];
[buffer commit];
@ -227,6 +229,7 @@ public:
}
HRESULT BeginDrawing(IAvnMetalRenderingSession **ret) override {
START_COM_ARP_CALL;
if([NSThread isMainThread])
{
// Flush all existing rendering
@ -289,7 +292,7 @@ class AvnMetalDisplay : public ComSingleObject<IAvnMetalDisplay, &IID_IAvnMetalD
public:
FORWARD_IUNKNOWN()
HRESULT CreateDevice(IAvnMetalDevice **ret) override {
START_COM_ARP_CALL;
auto device = MTLCreateSystemDefaultDevice();
if(device == nil) {
ret = nil;

6
nukebuild/_build.csproj

@ -12,6 +12,7 @@
<ItemGroup>
<PackageReference Include="Nuke.Common" />
<PackageReference Include="MicroCom.CodeGenerator" />
<PackageReference Include="Numerge" />
<!-- Keep in sync with Avalonia.Build.Tasks -->
<PackageReference Include="Mono.Cecil" />
<PackageReference Include="Microsoft.Build.Framework" PrivateAssets="All" />
@ -24,11 +25,6 @@
<ItemGroup>
<NukeMetadata Include="**\*.json" Exclude="bin\**;obj\**" />
<NukeExternalFiles Include="**\*.*.ext" Exclude="bin\**;obj\**" />
<!-- Common build related files -->
<Compile Include="../external/Numerge/Numerge/**/*.cs"
Exclude="../external/Numerge/Numerge/obj/**/*.cs"
LinkBase="Numerge" />
<EmbeddedResource Include="../build/avalonia.snk" />
</ItemGroup>

3
samples/ControlCatalog/MainView.xaml

@ -161,6 +161,9 @@
<TabItem Header="OpenGL Lease">
<pages:OpenGlLeasePage />
</TabItem>
<TabItem Header="PipsPager">
<pages:PipsPagerPage />
</TabItem>
<TabItem Header="Platform Information">
<pages:PlatformInfoPage />
</TabItem>

5
samples/ControlCatalog/Pages/ClipboardPage.xaml.cs

@ -34,7 +34,10 @@ namespace ControlCatalog.Pages
{
InitializeComponent();
_clipboardLastDataObjectChecker =
new DispatcherTimer(TimeSpan.FromSeconds(0.5), default, CheckLastDataObject);
new DispatcherTimer(TimeSpan.FromSeconds(0.5), default, CheckLastDataObject)
{
IsEnabled = false
};
using var asset = AssetLoader.Open(new Uri("avares://ControlCatalog/Assets/image1.jpg"));
_defaultImage = new Bitmap(asset);

14
samples/ControlCatalog/Pages/DialogsPage.xaml.cs

@ -228,13 +228,8 @@ namespace ControlCatalog.Pages
try
{
// Sync disposal of StreamWriter is not supported on WASM
#if NET6_0_OR_GREATER
await using var stream = await file.OpenWriteAsync();
await using var writer = new System.IO.StreamWriter(stream);
#else
using var stream = await file.OpenWriteAsync();
using var writer = new System.IO.StreamWriter(stream);
#endif
await writer.WriteLineAsync(openedFileContent.Text);
SetFolder(await file.GetParentAsync());
@ -265,13 +260,8 @@ namespace ControlCatalog.Pages
if (result.File is { } file)
{
// Sync disposal of StreamWriter is not supported on WASM
#if NET6_0_OR_GREATER
await using var stream = await file.OpenWriteAsync();
await using var writer = new System.IO.StreamWriter(stream);
#else
using var stream = await file.OpenWriteAsync();
using var writer = new System.IO.StreamWriter(stream);
#endif
if (result.SelectedFileType == FilePickerFileTypes.Xml)
{
await writer.WriteLineAsync("<sample>Test</sample>");
@ -431,11 +421,7 @@ namespace ControlCatalog.Pages
internal static async Task<string> ReadTextFromFile(IStorageFile file, int length)
{
#if NET6_0_OR_GREATER
await using var stream = await file.OpenReadAsync();
#else
using var stream = await file.OpenReadAsync();
#endif
using var reader = new System.IO.StreamReader(stream);
// 4GB file test, shouldn't load more than 10000 chars into a memory.

50
samples/ControlCatalog/Pages/PipsPager/PipsPagerCarouselPage.xaml

@ -0,0 +1,50 @@
<UserControl xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
x:Class="ControlCatalog.Pages.PipsPagerCarouselPage">
<DockPanel>
<ScrollViewer DockPanel.Dock="Right" Width="220">
<StackPanel Margin="12" Spacing="8">
<TextBlock Text="Carousel Integration" FontSize="16" FontWeight="SemiBold"
Foreground="{DynamicResource SystemControlHighlightAccentBrush}" />
<TextBlock TextWrapping="Wrap" FontSize="12" Opacity="0.7"
Text="Bind SelectedPageIndex to a Carousel's SelectedIndex for two-way synchronized page navigation." />
<Separator />
<TextBlock Text="Binding" FontSize="13" FontWeight="SemiBold" />
<TextBlock FontSize="12" TextWrapping="Wrap"
Text="SelectedIndex='{Binding #Pager.SelectedPageIndex, Mode=TwoWay}'" />
</StackPanel>
</ScrollViewer>
<Border DockPanel.Dock="Right" Width="1"
Background="{DynamicResource SystemControlForegroundBaseMediumLowBrush}" />
<Grid RowDefinitions="*,Auto" Margin="24">
<Carousel Name="GalleryCarousel"
SelectedIndex="{Binding #GalleryPager.SelectedPageIndex, Mode=TwoWay}">
<Carousel.Items>
<Border Background="#E3F2FD" CornerRadius="8">
<TextBlock Text="Page 1" VerticalAlignment="Center" HorizontalAlignment="Center" FontSize="30" Opacity="0.6" />
</Border>
<Border Background="#C8E6C9" CornerRadius="8">
<TextBlock Text="Page 2" VerticalAlignment="Center" HorizontalAlignment="Center" FontSize="30" Opacity="0.6" />
</Border>
<Border Background="#FFE0B2" CornerRadius="8">
<TextBlock Text="Page 3" VerticalAlignment="Center" HorizontalAlignment="Center" FontSize="30" Opacity="0.6" />
</Border>
<Border Background="#E1BEE7" CornerRadius="8">
<TextBlock Text="Page 4" VerticalAlignment="Center" HorizontalAlignment="Center" FontSize="30" Opacity="0.6" />
</Border>
<Border Background="#FFCDD2" CornerRadius="8">
<TextBlock Text="Page 5" VerticalAlignment="Center" HorizontalAlignment="Center" FontSize="30" Opacity="0.6" />
</Border>
</Carousel.Items>
</Carousel>
<PipsPager Name="GalleryPager"
Grid.Row="1"
NumberOfPages="5"
HorizontalAlignment="Center"
Margin="0,12,0,0" />
</Grid>
</DockPanel>
</UserControl>

11
samples/ControlCatalog/Pages/PipsPager/PipsPagerCarouselPage.xaml.cs

@ -0,0 +1,11 @@
using Avalonia.Controls;
namespace ControlCatalog.Pages;
public partial class PipsPagerCarouselPage : UserControl
{
public PipsPagerCarouselPage()
{
InitializeComponent();
}
}

78
samples/ControlCatalog/Pages/PipsPager/PipsPagerCustomButtonsPage.xaml

@ -0,0 +1,78 @@
<UserControl xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
x:Class="ControlCatalog.Pages.PipsPagerCustomButtonsPage">
<DockPanel>
<ScrollViewer DockPanel.Dock="Right" Width="220">
<StackPanel Margin="12" Spacing="8">
<TextBlock Text="Custom Buttons" FontSize="16" FontWeight="SemiBold"
Foreground="{DynamicResource SystemControlHighlightAccentBrush}" />
<TextBlock TextWrapping="Wrap" FontSize="12" Opacity="0.7"
Text="Replace the default chevron navigation buttons with custom styled buttons using PreviousButtonStyle and NextButtonStyle." />
<Separator />
<TextBlock Text="Properties" FontSize="13" FontWeight="SemiBold" />
<TextBlock FontSize="12" TextWrapping="Wrap" Text="PreviousButtonStyle" />
<TextBlock FontSize="12" TextWrapping="Wrap" Text="NextButtonStyle" />
<TextBlock FontSize="12" TextWrapping="Wrap" Text="IsPreviousButtonVisible" />
<TextBlock FontSize="12" TextWrapping="Wrap" Text="IsNextButtonVisible" />
</StackPanel>
</ScrollViewer>
<Border DockPanel.Dock="Right" Width="1"
Background="{DynamicResource SystemControlForegroundBaseMediumLowBrush}" />
<StackPanel Spacing="24" Margin="24">
<StackPanel Spacing="8">
<TextBlock Text="Text Buttons" FontWeight="SemiBold" FontSize="14" />
<PipsPager NumberOfPages="5" MaxVisiblePips="5"
IsPreviousButtonVisible="True" IsNextButtonVisible="True">
<PipsPager.Resources>
<ControlTheme x:Key="CustomPreviousButtonStyle" TargetType="Button">
<Setter Property="Content" Value="Prev" />
<Setter Property="Background" Value="LightGray" />
<Setter Property="Foreground" Value="Black" />
<Setter Property="Padding" Value="8,2" />
<Setter Property="Margin" Value="0,0,8,0" />
<Setter Property="Template">
<ControlTemplate>
<Border Background="{TemplateBinding Background}" CornerRadius="4">
<ContentPresenter Content="{TemplateBinding Content}" Margin="{TemplateBinding Padding}" />
</Border>
</ControlTemplate>
</Setter>
</ControlTheme>
<ControlTheme x:Key="CustomNextButtonStyle" TargetType="Button">
<Setter Property="Content" Value="Next" />
<Setter Property="Background" Value="LightGray" />
<Setter Property="Foreground" Value="Black" />
<Setter Property="Padding" Value="8,2" />
<Setter Property="Margin" Value="8,0,0,0" />
<Setter Property="Template">
<ControlTemplate>
<Border Background="{TemplateBinding Background}" CornerRadius="4">
<ContentPresenter Content="{TemplateBinding Content}" Margin="{TemplateBinding Padding}" />
</Border>
</ControlTemplate>
</Setter>
</ControlTheme>
</PipsPager.Resources>
<PipsPager.PreviousButtonStyle>
<StaticResource ResourceKey="CustomPreviousButtonStyle" />
</PipsPager.PreviousButtonStyle>
<PipsPager.NextButtonStyle>
<StaticResource ResourceKey="CustomNextButtonStyle" />
</PipsPager.NextButtonStyle>
</PipsPager>
</StackPanel>
<StackPanel Spacing="8">
<TextBlock Text="Hidden Buttons" FontWeight="SemiBold" FontSize="14" />
<PipsPager NumberOfPages="7"
MaxVisiblePips="7"
IsPreviousButtonVisible="False"
IsNextButtonVisible="False" />
</StackPanel>
</StackPanel>
</DockPanel>
</UserControl>

11
samples/ControlCatalog/Pages/PipsPager/PipsPagerCustomButtonsPage.xaml.cs

@ -0,0 +1,11 @@
using Avalonia.Controls;
namespace ControlCatalog.Pages;
public partial class PipsPagerCustomButtonsPage : UserControl
{
public PipsPagerCustomButtonsPage()
{
InitializeComponent();
}
}

59
samples/ControlCatalog/Pages/PipsPager/PipsPagerCustomColorsPage.xaml

@ -0,0 +1,59 @@
<UserControl xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
x:Class="ControlCatalog.Pages.PipsPagerCustomColorsPage">
<DockPanel>
<ScrollViewer DockPanel.Dock="Right" Width="220">
<StackPanel Margin="12" Spacing="8">
<TextBlock Text="Custom Colors" FontSize="16" FontWeight="SemiBold"
Foreground="{DynamicResource SystemControlHighlightAccentBrush}" />
<TextBlock TextWrapping="Wrap" FontSize="12" Opacity="0.7"
Text="Override pip indicator colors using resource keys." />
<Separator />
<TextBlock Text="Resource Keys" FontSize="13" FontWeight="SemiBold" />
<TextBlock FontSize="11" TextWrapping="Wrap" Text="PipsPagerSelectionIndicatorForeground" />
<TextBlock FontSize="11" TextWrapping="Wrap" Text="PipsPagerSelectionIndicatorForegroundSelected" />
<TextBlock FontSize="11" TextWrapping="Wrap" Text="PipsPagerSelectionIndicatorForegroundPointerOver" />
</StackPanel>
</ScrollViewer>
<Border DockPanel.Dock="Right" Width="1"
Background="{DynamicResource SystemControlForegroundBaseMediumLowBrush}" />
<StackPanel Spacing="24" Margin="24">
<StackPanel Spacing="8">
<TextBlock Text="Orange / Blue" FontWeight="SemiBold" FontSize="14" />
<PipsPager NumberOfPages="5" MaxVisiblePips="5">
<PipsPager.Resources>
<SolidColorBrush x:Key="PipsPagerSelectionIndicatorForeground" Color="Orange" />
<SolidColorBrush x:Key="PipsPagerSelectionIndicatorForegroundSelected" Color="Blue" />
<SolidColorBrush x:Key="PipsPagerSelectionIndicatorForegroundPointerOver" Color="Gold" />
</PipsPager.Resources>
</PipsPager>
</StackPanel>
<StackPanel Spacing="8">
<TextBlock Text="Green / Red" FontWeight="SemiBold" FontSize="14" />
<PipsPager NumberOfPages="5" MaxVisiblePips="5">
<PipsPager.Resources>
<SolidColorBrush x:Key="PipsPagerSelectionIndicatorForeground" Color="#81C784" />
<SolidColorBrush x:Key="PipsPagerSelectionIndicatorForegroundSelected" Color="#E53935" />
<SolidColorBrush x:Key="PipsPagerSelectionIndicatorForegroundPointerOver" Color="#A5D6A7" />
</PipsPager.Resources>
</PipsPager>
</StackPanel>
<StackPanel Spacing="8">
<TextBlock Text="Purple / Teal" FontWeight="SemiBold" FontSize="14" />
<PipsPager NumberOfPages="5" MaxVisiblePips="5">
<PipsPager.Resources>
<SolidColorBrush x:Key="PipsPagerSelectionIndicatorForeground" Color="#CE93D8" />
<SolidColorBrush x:Key="PipsPagerSelectionIndicatorForegroundSelected" Color="#00897B" />
<SolidColorBrush x:Key="PipsPagerSelectionIndicatorForegroundPointerOver" Color="#BA68C8" />
</PipsPager.Resources>
</PipsPager>
</StackPanel>
</StackPanel>
</DockPanel>
</UserControl>

11
samples/ControlCatalog/Pages/PipsPager/PipsPagerCustomColorsPage.xaml.cs

@ -0,0 +1,11 @@
using Avalonia.Controls;
namespace ControlCatalog.Pages;
public partial class PipsPagerCustomColorsPage : UserControl
{
public PipsPagerCustomColorsPage()
{
InitializeComponent();
}
}

197
samples/ControlCatalog/Pages/PipsPager/PipsPagerCustomTemplatesPage.xaml

@ -0,0 +1,197 @@
<UserControl xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
x:Class="ControlCatalog.Pages.PipsPagerCustomTemplatesPage">
<DockPanel>
<ScrollViewer DockPanel.Dock="Right" Width="220">
<StackPanel Margin="12" Spacing="8">
<TextBlock Text="Custom Templates" FontSize="16" FontWeight="SemiBold"
Foreground="{DynamicResource SystemControlHighlightAccentBrush}" />
<TextBlock TextWrapping="Wrap" FontSize="12" Opacity="0.7"
Text="Override pip item templates using Style selectors targeting the inner ListBoxItem to create squares, pills, numbers, or any custom shape." />
<Separator />
<TextBlock Text="Technique" FontSize="13" FontWeight="SemiBold" />
<TextBlock FontSize="12" TextWrapping="Wrap"
Text="Target: PipsPager /template/ ListBox ListBoxItem" />
<TextBlock FontSize="12" TextWrapping="Wrap"
Text="States: :selected, :pointerover, :pressed" />
</StackPanel>
</ScrollViewer>
<Border DockPanel.Dock="Right" Width="1"
Background="{DynamicResource SystemControlForegroundBaseMediumLowBrush}" />
<StackPanel Spacing="32" Margin="24">
<!-- Squares -->
<StackPanel Spacing="8">
<TextBlock Text="Squares" FontWeight="SemiBold" FontSize="14" />
<PipsPager NumberOfPages="5">
<PipsPager.Styles>
<Style Selector="PipsPager /template/ ListBox ListBoxItem">
<Setter Property="Template">
<ControlTemplate>
<Grid Background="Transparent">
<Rectangle Name="Pip"
Width="4" Height="4"
HorizontalAlignment="Center" VerticalAlignment="Center"
Fill="{DynamicResource PipsPagerSelectionIndicatorForeground}">
<Rectangle.Transitions>
<Transitions>
<DoubleTransition Property="Width" Duration="0:0:0.167" />
<DoubleTransition Property="Height" Duration="0:0:0.167" />
<BrushTransition Property="Fill" Duration="0:0:0.167" />
</Transitions>
</Rectangle.Transitions>
</Rectangle>
</Grid>
</ControlTemplate>
</Setter>
</Style>
<Style Selector="PipsPager /template/ ListBox ListBoxItem:pointerover /template/ Rectangle#Pip">
<Setter Property="Width" Value="6" />
<Setter Property="Height" Value="6" />
<Setter Property="Fill" Value="{DynamicResource PipsPagerSelectionIndicatorForegroundPointerOver}" />
</Style>
<Style Selector="PipsPager /template/ ListBox ListBoxItem:selected /template/ Rectangle#Pip">
<Setter Property="Width" Value="6" />
<Setter Property="Height" Value="6" />
<Setter Property="Fill" Value="{DynamicResource PipsPagerSelectionIndicatorForegroundSelected}" />
</Style>
<Style Selector="PipsPager /template/ ListBox ListBoxItem:selected:pointerover /template/ Rectangle#Pip">
<Setter Property="Width" Value="6" />
<Setter Property="Height" Value="6" />
<Setter Property="Fill" Value="{DynamicResource PipsPagerSelectionIndicatorForegroundSelected}" />
</Style>
<Style Selector="PipsPager /template/ ListBox ListBoxItem:pressed /template/ Rectangle#Pip">
<Setter Property="Width" Value="4" />
<Setter Property="Height" Value="4" />
<Setter Property="Fill" Value="{DynamicResource PipsPagerSelectionIndicatorForegroundPressed}" />
</Style>
</PipsPager.Styles>
</PipsPager>
</StackPanel>
<!-- Pill-shaped -->
<StackPanel Spacing="8">
<TextBlock Text="Pill-shaped Selected" FontWeight="SemiBold" FontSize="14" />
<PipsPager NumberOfPages="5"
IsPreviousButtonVisible="False"
IsNextButtonVisible="False">
<PipsPager.Styles>
<Style Selector="PipsPager /template/ ListBox ListBoxItem">
<Setter Property="Width" Value="24" />
<Setter Property="Height" Value="24" />
<Setter Property="Padding" Value="0" />
<Setter Property="Margin" Value="2,0" />
<Setter Property="MinWidth" Value="0" />
<Setter Property="MinHeight" Value="0" />
<Setter Property="ClipToBounds" Value="False" />
<Setter Property="VerticalAlignment" Value="Center" />
<Setter Property="Template">
<ControlTemplate>
<Grid Background="Transparent">
<Border Name="Pip"
Width="8" Height="8" CornerRadius="4"
HorizontalAlignment="Center" VerticalAlignment="Center"
Background="#C0C0C0">
<Border.Transitions>
<Transitions>
<DoubleTransition Property="Width" Duration="0:0:0.2" Easing="CubicEaseOut" />
<DoubleTransition Property="Height" Duration="0:0:0.2" Easing="CubicEaseOut" />
<CornerRadiusTransition Property="CornerRadius" Duration="0:0:0.2" Easing="CubicEaseOut" />
<BrushTransition Property="Background" Duration="0:0:0.2" />
</Transitions>
</Border.Transitions>
</Border>
</Grid>
</ControlTemplate>
</Setter>
</Style>
<Style Selector="PipsPager /template/ ListBox ListBoxItem:pointerover /template/ Border#Pip">
<Setter Property="Width" Value="10" />
<Setter Property="Height" Value="10" />
<Setter Property="CornerRadius" Value="5" />
<Setter Property="Background" Value="#909090" />
</Style>
<Style Selector="PipsPager /template/ ListBox ListBoxItem:selected /template/ Border#Pip">
<Setter Property="Width" Value="24" />
<Setter Property="Height" Value="8" />
<Setter Property="CornerRadius" Value="4" />
<Setter Property="Background" Value="#FF6B35" />
</Style>
<Style Selector="PipsPager /template/ ListBox ListBoxItem:selected:pointerover /template/ Border#Pip">
<Setter Property="Width" Value="24" />
<Setter Property="Height" Value="8" />
<Setter Property="CornerRadius" Value="4" />
<Setter Property="Background" Value="#E85A2A" />
</Style>
<Style Selector="PipsPager /template/ ListBox ListBoxItem:pressed /template/ Border#Pip">
<Setter Property="Width" Value="8" />
<Setter Property="Height" Value="8" />
<Setter Property="Background" Value="#707070" />
</Style>
</PipsPager.Styles>
</PipsPager>
</StackPanel>
<!-- Numbers -->
<StackPanel Spacing="8">
<TextBlock Text="Numbers" FontWeight="SemiBold" FontSize="14" />
<PipsPager NumberOfPages="10"
MaxVisiblePips="4"
IsNextButtonVisible="True"
IsPreviousButtonVisible="True"
ClipToBounds="False">
<PipsPager.Styles>
<Style Selector="PipsPager /template/ ListBox ListBoxItem">
<Setter Property="Width" Value="44" />
<Setter Property="Height" Value="44" />
<Setter Property="Padding" Value="0" />
<Setter Property="Margin" Value="2" />
<Setter Property="ClipToBounds" Value="False" />
<Setter Property="Template">
<ControlTemplate>
<Border Name="PipBorder"
Width="30" Height="30"
VerticalAlignment="Center" HorizontalAlignment="Center"
CornerRadius="10" ClipToBounds="False"
Background="LightGray">
<Border.Transitions>
<Transitions>
<DoubleTransition Property="Width" Duration="0:0:0.2" Easing="CubicEaseOut" />
<DoubleTransition Property="Height" Duration="0:0:0.2" Easing="CubicEaseOut" />
<BrushTransition Property="Background" Duration="0:0:0.2" />
</Transitions>
</Border.Transitions>
<TextBlock Text="{TemplateBinding Content}"
VerticalAlignment="Center" HorizontalAlignment="Center"
Foreground="Black" FontWeight="SemiBold" />
</Border>
</ControlTemplate>
</Setter>
</Style>
<Style Selector="PipsPager /template/ ListBox ListBoxItem:pointerover /template/ Border#PipBorder">
<Setter Property="Background" Value="DarkGray" />
</Style>
<Style Selector="PipsPager /template/ ListBox ListBoxItem:pressed /template/ Border#PipBorder">
<Setter Property="Width" Value="28" />
<Setter Property="Height" Value="28" />
</Style>
<Style Selector="PipsPager /template/ ListBox ListBoxItem:selected /template/ Border#PipBorder">
<Setter Property="Background" Value="#0078D7" />
<Setter Property="Width" Value="36" />
<Setter Property="Height" Value="36" />
</Style>
<Style Selector="PipsPager /template/ ListBox ListBoxItem:selected /template/ TextBlock">
<Setter Property="Foreground" Value="White" />
</Style>
<Style Selector="PipsPager /template/ ListBox ListBoxItem:selected:pointerover /template/ Border#PipBorder">
<Setter Property="Background" Value="#106EBE" />
</Style>
</PipsPager.Styles>
</PipsPager>
</StackPanel>
</StackPanel>
</DockPanel>
</UserControl>

11
samples/ControlCatalog/Pages/PipsPager/PipsPagerCustomTemplatesPage.xaml.cs

@ -0,0 +1,11 @@
using Avalonia.Controls;
namespace ControlCatalog.Pages;
public partial class PipsPagerCustomTemplatesPage : UserControl
{
public PipsPagerCustomTemplatesPage()
{
InitializeComponent();
}
}

35
samples/ControlCatalog/Pages/PipsPager/PipsPagerEventsPage.xaml

@ -0,0 +1,35 @@
<UserControl xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
x:Class="ControlCatalog.Pages.PipsPagerEventsPage">
<DockPanel>
<ScrollViewer DockPanel.Dock="Right" Width="220">
<StackPanel Margin="12" Spacing="8">
<TextBlock Text="Events" FontSize="16" FontWeight="SemiBold"
Foreground="{DynamicResource SystemControlHighlightAccentBrush}" />
<TextBlock TextWrapping="Wrap" FontSize="12" Opacity="0.7"
Text="Monitor SelectedPageIndex changes to react to user navigation." />
<Separator />
<TextBlock Text="Event Log" FontSize="13" FontWeight="SemiBold" />
<ItemsControl Name="EventLog">
<ItemsControl.ItemTemplate>
<DataTemplate>
<TextBlock Text="{Binding}" FontSize="11" TextWrapping="Wrap" />
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</StackPanel>
</ScrollViewer>
<Border DockPanel.Dock="Right" Width="1"
Background="{DynamicResource SystemControlForegroundBaseMediumLowBrush}" />
<StackPanel Spacing="16" Margin="24">
<TextBlock Text="Tap a pip and watch the event log" FontSize="14" Opacity="0.6" />
<PipsPager Name="EventPager"
NumberOfPages="8"
MaxVisiblePips="8" />
<TextBlock Name="StatusText" FontSize="13" Opacity="0.7"
Text="Selected: 0" />
</StackPanel>
</DockPanel>
</UserControl>

29
samples/ControlCatalog/Pages/PipsPager/PipsPagerEventsPage.xaml.cs

@ -0,0 +1,29 @@
using System.Collections.ObjectModel;
using Avalonia.Controls;
namespace ControlCatalog.Pages;
public partial class PipsPagerEventsPage : UserControl
{
private readonly ObservableCollection<string> _events = new();
public PipsPagerEventsPage()
{
InitializeComponent();
EventLog.ItemsSource = _events;
EventPager.PropertyChanged += (_, e) =>
{
if (e.Property != PipsPager.SelectedPageIndexProperty)
return;
var newIndex = (int)e.NewValue!;
StatusText.Text = $"Selected: {newIndex}";
_events.Insert(0, $"SelectedPageIndex changed to {newIndex}");
if (_events.Count > 20)
_events.RemoveAt(_events.Count - 1);
};
}
}

46
samples/ControlCatalog/Pages/PipsPager/PipsPagerGettingStartedPage.xaml

@ -0,0 +1,46 @@
<UserControl xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
x:Class="ControlCatalog.Pages.PipsPagerGettingStartedPage">
<DockPanel>
<ScrollViewer DockPanel.Dock="Right" Width="220">
<StackPanel Margin="12" Spacing="8">
<TextBlock Text="Getting Started" FontSize="16" FontWeight="SemiBold"
Foreground="{DynamicResource SystemControlHighlightAccentBrush}" />
<TextBlock TextWrapping="Wrap" FontSize="12" Opacity="0.7"
Text="PipsPager lets users navigate a paginated collection using configurable dot indicators with optional navigation buttons." />
<Separator />
<TextBlock Text="Features" FontSize="13" FontWeight="SemiBold" />
<TextBlock FontSize="12" TextWrapping="Wrap" Text="Configurable maximum visible pips" />
<TextBlock FontSize="12" TextWrapping="Wrap" Text="Automatic scrolling for large collections" />
<TextBlock FontSize="12" TextWrapping="Wrap" Text="Horizontal and Vertical orientation" />
<TextBlock FontSize="12" TextWrapping="Wrap" Text="Previous/Next navigation buttons" />
<TextBlock FontSize="12" TextWrapping="Wrap" Text="Customizable pips and buttons" />
</StackPanel>
</ScrollViewer>
<Border DockPanel.Dock="Right" Width="1"
Background="{DynamicResource SystemControlForegroundBaseMediumLowBrush}" />
<StackPanel Spacing="24" Margin="24">
<StackPanel Spacing="8">
<TextBlock Text="Default" FontWeight="SemiBold" FontSize="14" />
<PipsPager NumberOfPages="5" />
</StackPanel>
<StackPanel Spacing="8">
<TextBlock Text="Without Navigation Buttons" FontWeight="SemiBold" FontSize="14" />
<PipsPager NumberOfPages="10"
IsPreviousButtonVisible="False"
IsNextButtonVisible="False"
MaxVisiblePips="5" />
</StackPanel>
<StackPanel Spacing="8">
<TextBlock Text="Vertical" FontWeight="SemiBold" FontSize="14" />
<PipsPager NumberOfPages="5" Orientation="Vertical" />
</StackPanel>
</StackPanel>
</DockPanel>
</UserControl>

11
samples/ControlCatalog/Pages/PipsPager/PipsPagerGettingStartedPage.xaml.cs

@ -0,0 +1,11 @@
using Avalonia.Controls;
namespace ControlCatalog.Pages;
public partial class PipsPagerGettingStartedPage : UserControl
{
public PipsPagerGettingStartedPage()
{
InitializeComponent();
}
}

52
samples/ControlCatalog/Pages/PipsPager/PipsPagerLargeCollectionPage.xaml

@ -0,0 +1,52 @@
<UserControl xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
x:Class="ControlCatalog.Pages.PipsPagerLargeCollectionPage">
<DockPanel>
<ScrollViewer DockPanel.Dock="Right" Width="220">
<StackPanel Margin="12" Spacing="8">
<TextBlock Text="Large Collections" FontSize="16" FontWeight="SemiBold"
Foreground="{DynamicResource SystemControlHighlightAccentBrush}" />
<TextBlock TextWrapping="Wrap" FontSize="12" Opacity="0.7"
Text="Use MaxVisiblePips to limit visible indicators when the page count is large. The pips scroll automatically to keep the selected pip visible." />
<Separator />
<TextBlock Text="Properties" FontSize="13" FontWeight="SemiBold" />
<TextBlock FontSize="12" TextWrapping="Wrap" Text="NumberOfPages: Total page count" />
<TextBlock FontSize="12" TextWrapping="Wrap" Text="MaxVisiblePips: Visible indicator limit" />
<TextBlock FontSize="12" TextWrapping="Wrap" Text="SelectedPageIndex: Current selection" />
</StackPanel>
</ScrollViewer>
<Border DockPanel.Dock="Right" Width="1"
Background="{DynamicResource SystemControlForegroundBaseMediumLowBrush}" />
<StackPanel Spacing="24" Margin="24">
<StackPanel Spacing="8">
<TextBlock Text="50 Pages, MaxVisiblePips=7" FontWeight="SemiBold" FontSize="14" />
<PipsPager Name="LargePager"
NumberOfPages="50"
MaxVisiblePips="7"
SelectedPageIndex="25" />
<TextBlock HorizontalAlignment="Left" FontSize="12">
<Run Text="Selected: " FontWeight="SemiBold" />
<Run Text="{Binding #LargePager.SelectedPageIndex}" />
<Run Text=" / " />
<Run Text="{Binding #LargePager.NumberOfPages}" />
</TextBlock>
</StackPanel>
<StackPanel Spacing="8">
<TextBlock Text="20 Pages, MaxVisiblePips=5" FontWeight="SemiBold" FontSize="14" />
<PipsPager NumberOfPages="20"
MaxVisiblePips="5" />
</StackPanel>
<StackPanel Spacing="8">
<TextBlock Text="100 Pages, MaxVisiblePips=9" FontWeight="SemiBold" FontSize="14" />
<PipsPager NumberOfPages="100"
MaxVisiblePips="9" />
</StackPanel>
</StackPanel>
</DockPanel>
</UserControl>

11
samples/ControlCatalog/Pages/PipsPager/PipsPagerLargeCollectionPage.xaml.cs

@ -0,0 +1,11 @@
using Avalonia.Controls;
namespace ControlCatalog.Pages;
public partial class PipsPagerLargeCollectionPage : UserControl
{
public PipsPagerLargeCollectionPage()
{
InitializeComponent();
}
}

11
samples/ControlCatalog/Pages/PipsPagerPage.xaml

@ -0,0 +1,11 @@
<UserControl xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
x:Class="ControlCatalog.Pages.PipsPagerPage">
<NavigationPage x:Name="SampleNav">
<NavigationPage.Styles>
<Style Selector="NavigationPage#SampleNav /template/ Border#PART_NavigationBar">
<Setter Property="Background" Value="Transparent" />
</Style>
</NavigationPage.Styles>
</NavigationPage>
</UserControl>

47
samples/ControlCatalog/Pages/PipsPagerPage.xaml.cs

@ -0,0 +1,47 @@
using System;
using Avalonia.Controls;
using Avalonia.Interactivity;
namespace ControlCatalog.Pages
{
public partial class PipsPagerPage : UserControl
{
private static readonly (string Group, string Title, string Description, Func<UserControl> Factory)[] Demos =
{
("Getting Started", "First Look",
"Default PipsPager with horizontal and vertical orientation, with and without navigation buttons.",
() => new PipsPagerGettingStartedPage()),
("Features", "Carousel Integration",
"Bind SelectedPageIndex to a Carousel's SelectedIndex for two-way synchronized page navigation.",
() => new PipsPagerCarouselPage()),
("Features", "Large Collections",
"Use MaxVisiblePips to limit visible indicators when the page count is large. Pips scroll automatically.",
() => new PipsPagerLargeCollectionPage()),
("Features", "Events",
"Monitor SelectedPageIndex changes to react to user navigation.",
() => new PipsPagerEventsPage()),
("Appearance", "Custom Colors",
"Override pip indicator colors using resource keys for normal, selected, and hover states.",
() => new PipsPagerCustomColorsPage()),
("Appearance", "Custom Buttons",
"Replace the default chevron navigation buttons with custom styled buttons.",
() => new PipsPagerCustomButtonsPage()),
("Appearance", "Custom Templates",
"Override pip item templates to create squares, pills, numbers, or any custom shape.",
() => new PipsPagerCustomTemplatesPage()),
};
public PipsPagerPage()
{
InitializeComponent();
Loaded += OnLoaded;
}
private async void OnLoaded(object? sender, RoutedEventArgs e)
{
await SampleNav.PushAsync(NavigationDemoHelper.CreateGalleryHomePage(SampleNav, Demos), null);
}
}
}

14
samples/SampleControls/HamburgerMenu/HamburgerMenu.xaml

@ -73,7 +73,8 @@
CornerRadius="{TemplateBinding CornerRadius}"
TextElement.FontFamily="{TemplateBinding FontFamily}"
TextElement.FontSize="{TemplateBinding FontSize}"
TextElement.FontWeight="{TemplateBinding FontWeight}" />
TextElement.FontWeight="{TemplateBinding FontWeight}"
AutomationProperties.LandmarkType="Main" />
</ControlTemplate>
</Setter>
@ -182,7 +183,9 @@
HorizontalScrollBarVisibility="{TemplateBinding (ScrollViewer.HorizontalScrollBarVisibility)}"
VerticalScrollBarVisibility="{TemplateBinding (ScrollViewer.VerticalScrollBarVisibility)}">
<ItemsPresenter Name="PART_ItemsPresenter"
HorizontalAlignment="Stretch">
HorizontalAlignment="Stretch"
AutomationProperties.ControlTypeOverride="List"
AutomationProperties.LandmarkType="Navigation">
<ItemsPresenter.ItemsPanel>
<ItemsPanelTemplate>
<StackPanel x:Name="HamburgerItemsPanel"
@ -201,7 +204,9 @@
</SplitView.Pane>
<SplitView.Content>
<DockPanel>
<Border Height="{StaticResource HeaderHeight}" DockPanel.Dock="Top">
<Border Height="{StaticResource HeaderHeight}"
DockPanel.Dock="Top"
AutomationProperties.LandmarkType="Banner">
<TextBlock x:Name="HeaderHolder"
VerticalAlignment="Center"
Classes="h1"
@ -246,7 +251,8 @@
HorizontalContentAlignment="Center"
Theme="{StaticResource NavigationButton}"
CornerRadius="4"
IsChecked="{Binding #PART_NavigationPane.IsPaneOpen, Mode=TwoWay}">
IsChecked="{Binding #PART_NavigationPane.IsPaneOpen, Mode=TwoWay}"
AutomationProperties.ControlTypeOverride="ListItem">
<PathIcon Data="M3 17h18a1 1 0 0 1 .117 1.993L21 19H3a1 1 0 0 1-.117-1.993L3 17h18H3Zm0-6 18-.002a1 1 0 0 1 .117 1.993l-.117.007L3 13a1 1 0 0 1-.117-1.993L3 11l18-.002L3 11Zm0-6h18a1 1 0 0 1 .117 1.993L21 7H3a1 1 0 0 1-.117-1.993L3 5h18H3Z" Foreground="{TemplateBinding Foreground}" />
</ToggleButton>
</Panel>

4
src/Android/Avalonia.Android/AndroidPlatform.cs

@ -76,19 +76,21 @@ namespace Avalonia.Android
public static AndroidPlatformOptions? Options { get; private set; }
internal static Compositor? Compositor { get; private set; }
internal static ChoreographerTimer? Timer { get; private set; }
public static void Initialize()
{
Options = AvaloniaLocator.Current.GetService<AndroidPlatformOptions>() ?? new AndroidPlatformOptions();
Dispatcher.InitializeUIThreadDispatcher(new AndroidDispatcherImpl());
Timer = new ChoreographerTimer();
AvaloniaLocator.CurrentMutable
.Bind<ICursorFactory>().ToTransient<CursorFactory>()
.Bind<IWindowingPlatform>().ToConstant(new WindowingPlatformStub())
.Bind<IKeyboardDevice>().ToSingleton<AndroidKeyboardDevice>()
.Bind<IPlatformSettings>().ToSingleton<AndroidPlatformSettings>()
.Bind<IPlatformIconLoader>().ToSingleton<PlatformIconLoaderStub>()
.Bind<IRenderTimer>().ToConstant(new ChoreographerTimer())
.Bind<IRenderLoop>().ToConstant(RenderLoop.FromTimer(Timer))
.Bind<PlatformHotkeyConfiguration>().ToSingleton<PlatformHotkeyConfiguration>()
.Bind<KeyGestureFormatInfo>().ToConstant(new KeyGestureFormatInfo(new Dictionary<Key, string>() { }))
.Bind<IActivatableLifetime>().ToConstant(new AndroidActivatableLifetime());

2
src/Android/Avalonia.Android/AvaloniaView.cs

@ -100,7 +100,7 @@ namespace Avalonia.Android
return;
if (isVisible && _timerSubscription == null)
{
if (AvaloniaLocator.Current.GetService<IRenderTimer>() is ChoreographerTimer timer)
if (AndroidPlatform.Timer is { } timer)
{
_timerSubscription = timer.SubscribeView(this);
}

60
src/Android/Avalonia.Android/ChoreographerTimer.cs

@ -18,10 +18,9 @@ namespace Avalonia.Android
private readonly AutoResetEvent _event = new(false);
private readonly GCHandle _timerHandle;
private readonly HashSet<AvaloniaView> _views = new();
private Action<TimeSpan>? _tick;
private bool _pendingCallback;
private long _lastTime;
private int _count;
public ChoreographerTimer()
{
@ -40,28 +39,13 @@ namespace Avalonia.Android
public bool RunsInBackground => true;
public event Action<TimeSpan> Tick
public Action<TimeSpan>? Tick
{
add
get => _tick;
set
{
lock (_lock)
{
_tick += value;
_count++;
if (_count == 1)
{
PostFrameCallback(_choreographer.Task.Result, GCHandle.ToIntPtr(_timerHandle));
}
}
}
remove
{
lock (_lock)
{
_tick -= value;
_count--;
}
_tick = value;
PostFrameCallbackIfNeeded();
}
}
@ -70,20 +54,14 @@ namespace Avalonia.Android
lock (_lock)
{
_views.Add(view);
if (_views.Count == 1)
{
PostFrameCallback(_choreographer.Task.Result, GCHandle.ToIntPtr(_timerHandle));
}
PostFrameCallbackIfNeeded();
}
return Disposable.Create(
() =>
{
lock (_lock)
{
lock (_lock)
_views.Remove(view);
}
}
);
}
@ -109,14 +87,28 @@ namespace Avalonia.Android
}
}
private void PostFrameCallbackIfNeeded()
{
lock (_lock)
{
if(_pendingCallback)
return;
if (_tick == null || _views.Count == 0)
return;
_pendingCallback = true;
PostFrameCallback(_choreographer.Task.Result, GCHandle.ToIntPtr(_timerHandle));
}
}
private void DoFrameCallback(long frameTimeNanos, IntPtr data)
{
lock (_lock)
{
if (_count > 0 && _views.Count > 0)
{
PostFrameCallback(_choreographer.Task.Result, data);
}
_pendingCallback = false;
PostFrameCallbackIfNeeded();
_lastTime = frameTimeNanos;
_event.Set();
}

63
src/Avalonia.Base/Animation/AnimationInstance`1.cs

@ -7,7 +7,7 @@ using Avalonia.Data;
namespace Avalonia.Animation
{
/// <summary>
/// Handles interpolation and time-related functions
/// Handles interpolation and time-related functions
/// for keyframe animations.
/// </summary>
internal class AnimationInstance<T> : SingleSubscriberObservableBase<T>
@ -35,6 +35,8 @@ namespace Avalonia.Animation
private readonly IClock _baseClock;
private IClock? _clock;
private EventHandler<AvaloniaPropertyChangedEventArgs>? _propertyChangedDelegate;
private EventHandler? _visibilityChangedHandler;
private EventHandler<VisualTreeAttachmentEventArgs>? _detachedHandler;
public AnimationInstance(Animation animation, Animatable control, Animator<T> animator, IClock baseClock, Action? OnComplete, Func<double, T, T> Interpolator)
{
@ -80,11 +82,34 @@ namespace Avalonia.Animation
protected override void Unsubscribed()
{
// Guard against reentrancy: DoComplete() can trigger Unsubscribed() via the
// _onCompleteAction disposal chain, and then PublishCompleted() calls it again.
var timerSub = _timerSub;
_timerSub = null;
if (timerSub is null)
return;
// Animation may have been stopped before it has finished.
ApplyFinalFill();
_targetControl.PropertyChanged -= _propertyChangedDelegate;
_timerSub?.Dispose();
timerSub.Dispose();
if (_targetControl is Visual visual)
{
if (_visibilityChangedHandler is not null)
{
visual.IsEffectivelyVisibleChanged -= _visibilityChangedHandler;
_visibilityChangedHandler = null;
}
if (_detachedHandler is not null)
{
visual.DetachedFromVisualTree -= _detachedHandler;
_detachedHandler = null;
}
}
_clock!.PlayState = PlayState.Stop;
}
@ -92,6 +117,35 @@ namespace Avalonia.Animation
{
_clock = new Clock(_baseClock);
_timerSub = _clock.Subscribe(Step);
if (_targetControl is Visual visual)
{
_visibilityChangedHandler = (_, _) =>
{
if (_clock is null || _clock.PlayState == PlayState.Stop)
return;
if (visual.IsEffectivelyVisible)
{
if (_clock.PlayState == PlayState.Pause)
_clock.PlayState = PlayState.Run;
}
else
{
if (_clock.PlayState == PlayState.Run)
_clock.PlayState = PlayState.Pause;
}
};
visual.IsEffectivelyVisibleChanged += _visibilityChangedHandler;
// If already invisible when animation starts, pause immediately.
if (!visual.IsEffectivelyVisible)
_clock.PlayState = PlayState.Pause;
// Stop and dispose the animation when detached from the visual tree.
_detachedHandler = (_, _) => DoComplete();
visual.DetachedFromVisualTree += _detachedHandler;
}
_propertyChangedDelegate ??= ControlPropertyChanged;
_targetControl.PropertyChanged += _propertyChangedDelegate;
UpdateNeutralValue();
@ -101,7 +155,10 @@ namespace Avalonia.Animation
{
try
{
InternalStep(frameTick);
if (_clock?.PlayState == PlayState.Pause)
return;
InternalStep(frameTick);
}
catch (Exception e)
{

8
src/Avalonia.Base/Avalonia.Base.csproj

@ -16,11 +16,7 @@
<Import Project="..\..\build\DevAnalyzers.props" />
<Import Project="..\..\build\SourceGenerators.props" />
<ItemGroup>
<Compile Include="..\Shared\CallerArgumentExpressionAttribute.cs" Link="Compatibility\CallerArgumentExpressionAttribute.cs" />
<Compile Include="..\Shared\IsExternalInit.cs" Link="Compatibility\IsExternalInit.cs" />
<Compile Include="..\Shared\ModuleInitializer.cs" Link="Compatibility\ModuleInitializer.cs" />
<Compile Include="..\Shared\StringCompatibilityExtensions.cs" Link="Compatibility\StringCompatibilityExtensions.cs" />
<Compile Include="..\Shared\StreamCompatibilityExtensions.cs" Link="Compatibility\StreamCompatibilityExtensions.cs" />
</ItemGroup>
<ItemGroup Condition="!$([MSBuild]::IsTargetFrameworkCompatible('$(TargetFramework)', 'net6.0'))">
@ -65,8 +61,6 @@
</ItemGroup>
<ItemGroup Label="Build dependency">
<ProjectReference Include="$(MSBuildThisFileDirectory)\..\Avalonia.Build.Tasks\Avalonia.Build.Tasks.csproj"
ReferenceOutputAssembly="false"
PrivateAssets="all" />
<ProjectReference Include="$(MSBuildThisFileDirectory)\..\Avalonia.Build.Tasks\Avalonia.Build.Tasks.csproj" ReferenceOutputAssembly="false" PrivateAssets="all" />
</ItemGroup>
</Project>

32
src/Avalonia.Base/Compatibility/CollectionCompatibilityExtensions.cs

@ -1,32 +0,0 @@
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
namespace System;
#if !NET6_0_OR_GREATER
internal static class CollectionCompatibilityExtensions
{
public static bool Remove<TKey, TValue>(
this Dictionary<TKey, TValue> o,
TKey key,
[MaybeNullWhen(false)] out TValue value)
where TKey : notnull
{
if (o.TryGetValue(key, out value))
return o.Remove(key);
return false;
}
public static bool TryAdd<TKey, TValue>(this Dictionary<TKey, TValue> o, TKey key, TValue value)
where TKey : notnull
{
if (!o.ContainsKey(key))
{
o.Add(key, value);
return true;
}
return false;
}
}
#endif

122
src/Avalonia.Base/Compatibility/NativeLibrary.cs

@ -1,122 +0,0 @@
using System;
using System.ComponentModel;
using System.Reflection;
using System.Runtime.InteropServices;
using Avalonia.Compatibility;
using Avalonia.Platform.Interop;
namespace Avalonia.Compatibility
{
internal class NativeLibraryEx
{
#if NET6_0_OR_GREATER
public static IntPtr Load(string dll, Assembly assembly) => NativeLibrary.Load(dll, assembly, null);
public static IntPtr Load(string dll) => NativeLibrary.Load(dll);
public static bool TryGetExport(IntPtr handle, string name, out IntPtr address) =>
NativeLibrary.TryGetExport(handle, name, out address);
#else
public static IntPtr Load(string dll, Assembly assembly) => Load(dll);
public static IntPtr Load(string dll)
{
var handle = DlOpen!(dll);
if (handle != IntPtr.Zero)
return handle;
throw new InvalidOperationException("Unable to load " + dll, DlError!());
}
public static bool TryGetExport(IntPtr handle, string name, out IntPtr address)
{
try
{
address = DlSym!(handle, name);
return address != default;
}
catch (Exception)
{
address = default;
return false;
}
}
static NativeLibraryEx()
{
if (OperatingSystemEx.IsWindows())
{
Win32Imports.Init();
}
else if (OperatingSystemEx.IsLinux() || OperatingSystemEx.IsMacOS())
{
var buffer = Marshal.AllocHGlobal(0x1000);
uname(buffer);
var unixName = Marshal.PtrToStringAnsi(buffer);
Marshal.FreeHGlobal(buffer);
if (unixName == "Darwin")
OsXImports.Init();
else
LinuxImports.Init();
}
}
private static Func<string, IntPtr>? DlOpen;
private static Func<IntPtr, string, IntPtr>? DlSym;
private static Func<Exception?>? DlError;
[DllImport("libc")]
static extern int uname(IntPtr buf);
static class Win32Imports
{
[DllImport("kernel32", CharSet = CharSet.Ansi, ExactSpelling = true, SetLastError = true)]
private static extern IntPtr GetProcAddress(IntPtr hModule, string procName);
[DllImport("kernel32", EntryPoint = "LoadLibraryW", SetLastError = true, CharSet = CharSet.Unicode)]
private static extern IntPtr LoadLibrary(string lpszLib);
public static void Init()
{
DlOpen = LoadLibrary;
DlSym = GetProcAddress;
DlError = () => new Win32Exception(Marshal.GetLastWin32Error());
}
}
static class LinuxImports
{
[DllImport("libdl.so.2")]
private static extern IntPtr dlopen(string path, int flags);
[DllImport("libdl.so.2")]
private static extern IntPtr dlsym(IntPtr handle, string symbol);
[DllImport("libdl.so.2")]
private static extern IntPtr dlerror();
public static void Init()
{
DlOpen = s => dlopen(s, 1);
DlSym = dlsym;
DlError = () => new InvalidOperationException(Marshal.PtrToStringAnsi(dlerror()));
}
}
static class OsXImports
{
[DllImport("/usr/lib/libSystem.dylib")]
private static extern IntPtr dlopen(string path, int flags);
[DllImport("/usr/lib/libSystem.dylib")]
private static extern IntPtr dlsym(IntPtr handle, string symbol);
[DllImport("/usr/lib/libSystem.dylib")]
private static extern IntPtr dlerror();
public static void Init()
{
DlOpen = s => dlopen(s, 1);
DlSym = dlsym;
DlError = () => new InvalidOperationException(Marshal.PtrToStringAnsi(dlerror()));
}
}
#endif
}
}

32
src/Avalonia.Base/Compatibility/OperatingSystem.cs

@ -1,32 +0,0 @@
using System;
using System.Runtime.InteropServices;
namespace Avalonia.Compatibility
{
internal sealed class OperatingSystemEx
{
#if NET6_0_OR_GREATER
public static bool IsWindows() => OperatingSystem.IsWindows();
public static bool IsMacOS() => OperatingSystem.IsMacOS();
public static bool IsMacCatalyst() => OperatingSystem.IsMacCatalyst();
public static bool IsLinux() => OperatingSystem.IsLinux();
public static bool IsFreeBSD() => OperatingSystem.IsFreeBSD();
public static bool IsAndroid() => OperatingSystem.IsAndroid();
public static bool IsIOS() => OperatingSystem.IsIOS();
public static bool IsTvOS() => OperatingSystem.IsTvOS();
public static bool IsBrowser() => OperatingSystem.IsBrowser();
public static bool IsOSPlatform(string platform) => OperatingSystem.IsOSPlatform(platform);
#else
public static bool IsWindows() => RuntimeInformation.IsOSPlatform(OSPlatform.Windows);
public static bool IsMacOS() => RuntimeInformation.IsOSPlatform(OSPlatform.OSX);
public static bool IsLinux() => RuntimeInformation.IsOSPlatform(OSPlatform.Linux);
public static bool IsFreeBSD() => false;
public static bool IsAndroid() => false;
public static bool IsIOS() => false;
public static bool IsMacCatalyst() => false;
public static bool IsTvOS() => false;
public static bool IsBrowser() => false;
public static bool IsOSPlatform(string platform) => RuntimeInformation.IsOSPlatform(OSPlatform.Create(platform));
#endif
}
}

43
src/Avalonia.Base/Compatibility/StringSyntaxAttribute.cs

@ -1,43 +0,0 @@
#pragma warning disable MA0048 // File name must match type name
// https://github.com/dotnet/runtime/blob/v8.0.4/src/libraries/System.Private.CoreLib/src/System/Diagnostics/CodeAnalysis/StringSyntaxAttribute.cs
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
// ReSharper disable once CheckNamespace
namespace System.Diagnostics.CodeAnalysis
{
#if !NET7_0_OR_GREATER
/// <summary>Specifies the syntax used in a string.</summary>
[AttributeUsage(AttributeTargets.Parameter | AttributeTargets.Field | AttributeTargets.Property, AllowMultiple = false, Inherited = false)]
internal sealed class StringSyntaxAttribute : Attribute
{
/// <summary>Initializes the <see cref="StringSyntaxAttribute"/> with the identifier of the syntax used.</summary>
/// <param name="syntax">The syntax identifier.</param>
public StringSyntaxAttribute(string syntax)
{
Syntax = syntax;
Arguments = Array.Empty<object?>();
}
/// <summary>Initializes the <see cref="StringSyntaxAttribute"/> with the identifier of the syntax used.</summary>
/// <param name="syntax">The syntax identifier.</param>
/// <param name="arguments">Optional arguments associated with the specific syntax employed.</param>
public StringSyntaxAttribute(string syntax, params object?[] arguments)
{
Syntax = syntax;
Arguments = arguments;
}
/// <summary>Gets the identifier of the syntax used.</summary>
public string Syntax { get; }
/// <summary>Optional arguments associated with the specific syntax employed.</summary>
public object?[] Arguments { get; }
/// <summary>The syntax identifier for strings containing XML.</summary>
public const string Xml = nameof(Xml);
}
#endif
}

2
src/Avalonia.Base/Data/Core/ExpressionNodes/Reflection/DynamicPluginStreamNode.cs

@ -7,9 +7,7 @@ using Avalonia.Reactive;
namespace Avalonia.Data.Core.ExpressionNodes.Reflection;
[RequiresUnreferencedCode(TrimmingMessages.ExpressionNodeRequiresUnreferencedCodeMessage)]
#if NET8_0_OR_GREATER
[RequiresDynamicCode(TrimmingMessages.ExpressionNodeRequiresDynamicCodeMessage)]
#endif
internal sealed class DynamicPluginStreamNode : ExpressionNode
{
private IDisposable? _subscription;

2
src/Avalonia.Base/Data/Core/ExpressionNodes/Reflection/ExpressionTreeIndexerNode.cs

@ -16,9 +16,7 @@ internal sealed class ExpressionTreeIndexerNode : CollectionNodeBase, ISettableN
private readonly Delegate _getDelegate;
private readonly Delegate _firstArgumentDelegate;
#if NET8_0_OR_GREATER
[RequiresDynamicCode(TrimmingMessages.ExpressionNodeRequiresDynamicCodeMessage)]
#endif
public ExpressionTreeIndexerNode(IndexExpression expression)
{
var valueParameter = Expression.Parameter(expression.Type);

2
src/Avalonia.Base/Data/Core/Parsers/ExpressionNodeFactory.cs

@ -15,9 +15,7 @@ namespace Avalonia.Data.Core.Parsers
internal static class ExpressionNodeFactory
{
[RequiresUnreferencedCode(TrimmingMessages.ReflectionBindingRequiresUnreferencedCodeMessage)]
#if NET8_0_OR_GREATER
[RequiresDynamicCode(TrimmingMessages.ReflectionBindingRequiresDynamicCodeMessage)]
#endif
public static List<ExpressionNode>? CreateFromAst(
List<BindingExpressionGrammar.INode> astNodes,
Func<string?, string, Type?>? typeResolver,

4
src/Avalonia.Base/Data/Core/Plugins/BindingPlugins.cs

@ -32,11 +32,7 @@ namespace Avalonia.Data.Core.Plugins
{
// When building with AOT, don't create ReflectionMethodAccessorPlugin instance.
// This branch can be eliminated in compile time with AOT.
#if NET6_0_OR_GREATER
if (System.Runtime.CompilerServices.RuntimeFeature.IsDynamicCodeSupported)
#else
if (true)
#endif
{
s_propertyAccessors.Insert(1, new ReflectionMethodAccessorPlugin());
}

4
src/Avalonia.Base/Data/Core/Plugins/ReflectionMethodAccessorPlugin.cs

@ -7,9 +7,7 @@ using System.Reflection;
namespace Avalonia.Data.Core.Plugins
{
[RequiresUnreferencedCode(TrimmingMessages.PropertyAccessorsRequiresUnreferencedCodeMessage)]
#if NET8_0_OR_GREATER
[RequiresDynamicCode(TrimmingMessages.ExpressionNodeRequiresDynamicCodeMessage)]
#endif
internal class ReflectionMethodAccessorPlugin : IPropertyAccessorPlugin
{
private readonly Dictionary<(Type, string), MethodInfo?> _methodLookup =
@ -84,9 +82,7 @@ namespace Avalonia.Data.Core.Plugins
return found;
}
#if NET8_0_OR_GREATER
[RequiresDynamicCode(TrimmingMessages.ExpressionNodeRequiresDynamicCodeMessage)]
#endif
private sealed class Accessor : PropertyAccessorBase
{
public Accessor(WeakReference<object?> reference, MethodInfo method)

14
src/Avalonia.Base/Input/DataFormat.cs

@ -213,19 +213,7 @@ public abstract class DataFormat : IEquatable<DataFormat>
return true;
static bool IsValidChar(char c)
=> IsAsciiLetterOrDigit(c) || c == '.' || c == '-';
static bool IsAsciiLetterOrDigit(char c)
{
#if NET8_0_OR_GREATER
return char.IsAsciiLetterOrDigit(c);
#else
return c is
(>= '0' and <= '9') or
(>= 'A' and <= 'Z') or
(>= 'a' and <= 'z');
#endif
}
=> char.IsAsciiLetterOrDigit(c) || c == '.' || c == '-';
}
/// <inheritdoc />

37
src/Avalonia.Base/Input/FindNextElementOptions.cs

@ -6,12 +6,49 @@ using System.Threading.Tasks;
namespace Avalonia.Input
{
/// <summary>
/// Provides options to customize the behavior when identifying the next element to focus
/// during a navigation operation.
/// </summary>
public sealed class FindNextElementOptions
{
/// <summary>
/// Gets or sets the root <see cref="InputElement"/> within which the search for the next
/// focusable element will be conducted.
/// </summary>
/// <remarks>
/// This property defines the boundary for focus navigation operations. It determines the root element
/// in the visual tree under which the focusable item search is performed. If not specified, the search
/// will default to the current scope.
/// </remarks>
public InputElement? SearchRoot { get; init; }
/// <summary>
/// Gets or sets the rectangular region within the visual hierarchy that will be excluded
/// from consideration during focus navigation.
/// </summary>
public Rect ExclusionRect { get; init; }
/// <summary>
/// Gets or sets a rectangular region that serves as a hint for focus navigation.
/// This property specifies a rectangle, relative to the coordinate system of the search root,
/// which can be used as a preferred or prioritized target when navigating focus.
/// It can be null if no specific hint region is provided.
/// </summary>
public Rect? FocusHintRectangle { get; init; }
/// <summary>
/// Specifies an optional override for the navigation strategy used in XY focus navigation.
/// This property allows customizing the focus movement behavior when navigating between UI elements.
/// </summary>
public XYFocusNavigationStrategy? NavigationStrategyOverride { get; init; }
/// <summary>
/// Specifies whether occlusivity (overlapping of elements or obstructions)
/// should be ignored during focus navigation. When set to <c>true</c>,
/// the navigation logic disregards obstructions that may block a potential
/// focus target, allowing elements behind such obstructions to be considered.
/// </summary>
public bool IgnoreOcclusivity { get; init; }
}
}

39
src/Avalonia.Base/Input/FocusChangedEventArgs.cs

@ -0,0 +1,39 @@
using Avalonia.Interactivity;
namespace Avalonia.Input
{
/// <summary>
/// Represents the arguments of <see cref="InputElement.GotFocus"/> and <see cref="InputElement.LostFocus"/>.
/// </summary>
public class FocusChangedEventArgs : RoutedEventArgs, IKeyModifiersEventArgs
{
/// <summary>
/// Initializes a new instance of <see cref="FocusChangedEventArgs"/>.
/// </summary>
/// <param name="routedEvent">The routed event associated with these event args.</param>
public FocusChangedEventArgs(RoutedEvent routedEvent)
: base(routedEvent)
{
}
/// <summary>
/// Gets or sets the element that focus has moved to.
/// </summary>
public IInputElement? NewFocusedElement { get; init; }
/// <summary>
/// Gets or sets the element that previously had focus.
/// </summary>
public IInputElement? OldFocusedElement { get; init; }
/// <summary>
/// Gets or sets a value indicating how the change in focus occurred.
/// </summary>
public NavigationMethod NavigationMethod { get; init; }
/// <summary>
/// Gets or sets any key modifiers active at the time of focus.
/// </summary>
public KeyModifiers KeyModifiers { get; init; }
}
}

169
src/Avalonia.Base/Input/FocusManager.cs

@ -4,7 +4,6 @@ using System.Linq;
using Avalonia.Input.Navigation;
using Avalonia.Interactivity;
using Avalonia.Metadata;
using Avalonia.Reactive;
using Avalonia.VisualTree;
namespace Avalonia.Input
@ -12,7 +11,6 @@ namespace Avalonia.Input
/// <summary>
/// Manages focus for the application.
/// </summary>
[PrivateApi]
public class FocusManager : IFocusManager
{
/// <summary>
@ -42,58 +40,51 @@ namespace Avalonia.Input
RoutingStrategies.Tunnel);
}
[PrivateApi]
public FocusManager()
{
_contentRoot = null;
}
public FocusManager(IInputElement contentRoot)
{
_contentRoot = contentRoot;
}
internal void SetContentRoot(IInputElement? contentRoot)
/// <summary>
/// Gets or sets the content root for the focus management system.
/// </summary>
[PrivateApi]
public IInputElement? ContentRoot
{
_contentRoot = contentRoot;
get => _contentRoot;
set => _contentRoot = value;
}
private IInputElement? Current => KeyboardDevice.Instance?.FocusedElement;
private XYFocus _xyFocus = new();
private XYFocusOptions _xYFocusOptions = new XYFocusOptions();
private readonly XYFocus _xyFocus = new();
private IInputElement? _contentRoot;
private XYFocusOptions? _reusableFocusOptions;
/// <summary>
/// Gets the currently focused <see cref="IInputElement"/>.
/// </summary>
/// <inheritdoc />
public IInputElement? GetFocusedElement() => Current;
/// <summary>
/// Focuses a control.
/// </summary>
/// <param name="control">The control to focus.</param>
/// <param name="method">The method by which focus was changed.</param>
/// <param name="keyModifiers">Any key modifiers active at the time of focus.</param>
/// <inheritdoc />
public bool Focus(
IInputElement? control,
IInputElement? element,
NavigationMethod method = NavigationMethod.Unspecified,
KeyModifiers keyModifiers = KeyModifiers.None)
{
if (KeyboardDevice.Instance is not { } keyboardDevice)
return false;
if (control is not null)
if (element is not null)
{
if (!CanFocus(control))
if (!CanFocus(element))
return false;
if (GetFocusScope(control) is StyledElement scope)
if (GetFocusScope(element) is StyledElement scope)
{
scope.SetValue(FocusedElementProperty, control);
scope.SetValue(FocusedElementProperty, element);
_focusRoot = GetFocusRoot(scope);
}
keyboardDevice.SetFocusedElement(control, method, keyModifiers);
keyboardDevice.SetFocusedElement(element, method, keyModifiers);
return true;
}
else if (_focusRoot?.GetValue(FocusedElementProperty) is { } restore &&
@ -110,12 +101,7 @@ namespace Avalonia.Input
}
}
public void ClearFocus()
{
Focus(null);
}
public void ClearFocusOnElementRemoved(IInputElement removedElement, Visual oldParent)
internal void ClearFocusOnElementRemoved(IInputElement removedElement, Visual oldParent)
{
if (oldParent is IInputElement parentElement &&
GetFocusScope(parentElement) is StyledElement scope &&
@ -129,6 +115,7 @@ namespace Avalonia.Input
Focus(null);
}
[PrivateApi]
public IInputElement? GetFocusedElement(IFocusScope scope)
{
return (scope as StyledElement)?.GetValue(FocusedElementProperty);
@ -138,6 +125,7 @@ namespace Avalonia.Input
/// Notifies the focus manager of a change in focus scope.
/// </summary>
/// <param name="scope">The new focus scope.</param>
[PrivateApi]
public void SetFocusScope(IFocusScope scope)
{
if (GetFocusedElement(scope) is { } focused)
@ -153,12 +141,14 @@ namespace Avalonia.Input
}
}
[PrivateApi]
public void RemoveFocusRoot(IFocusScope scope)
{
if (scope == _focusRoot)
ClearFocus();
Focus(null);
}
[PrivateApi]
public static bool GetIsFocusScope(IInputElement e) => e is IFocusScope;
/// <summary>
@ -176,25 +166,15 @@ namespace Avalonia.Input
?? (FocusManager?)AvaloniaLocator.Current.GetService<IFocusManager>();
}
/// <summary>
/// Attempts to change focus from the element with focus to the next focusable element in the specified direction.
/// </summary>
/// <param name="direction">The direction to traverse (in tab order).</param>
/// <returns>true if focus moved; otherwise, false.</returns>
public bool TryMoveFocus(NavigationDirection direction)
/// <inheritdoc />
public bool TryMoveFocus(NavigationDirection direction, FindNextElementOptions? options = null)
{
return FindAndSetNextFocus(direction, _xYFocusOptions);
}
ValidateDirection(direction);
/// <summary>
/// Attempts to change focus from the element with focus to the next focusable element in the specified direction, using the specified navigation options.
/// </summary>
/// <param name="direction">The direction to traverse (in tab order).</param>
/// <param name="options">The options to help identify the next element to receive focus with keyboard/controller/remote navigation.</param>
/// <returns>true if focus moved; otherwise, false.</returns>
public bool TryMoveFocus(NavigationDirection direction, FindNextElementOptions options)
{
return FindAndSetNextFocus(direction, ValidateAndCreateFocusOptions(direction, options));
var focusOptions = ToFocusOptions(options, true);
var result = FindAndSetNextFocus(direction, focusOptions);
_reusableFocusOptions = focusOptions;
return result;
}
/// <summary>
@ -295,10 +275,7 @@ namespace Avalonia.Input
return true;
}
/// <summary>
/// Retrieves the first element that can receive focus.
/// </summary>
/// <returns>The first focusable element.</returns>
/// <inheritdoc />
public IInputElement? FindFirstFocusableElement()
{
var root = (_contentRoot as Visual)?.GetSelfAndVisualDescendants().FirstOrDefault(x => x is IInputElement) as IInputElement;
@ -317,10 +294,7 @@ namespace Avalonia.Input
return GetFirstFocusableElement(searchScope);
}
/// <summary>
/// Retrieves the last element that can receive focus.
/// </summary>
/// <returns>The last focusable element.</returns>
/// <inheritdoc />
public IInputElement? FindLastFocusableElement()
{
var root = (_contentRoot as Visual)?.GetSelfAndVisualDescendants().FirstOrDefault(x => x is IInputElement) as IInputElement;
@ -339,52 +313,59 @@ namespace Avalonia.Input
return GetFocusManager(searchScope)?.GetLastFocusableElement(searchScope);
}
/// <summary>
/// Retrieves the element that should receive focus based on the specified navigation direction.
/// </summary>
/// <param name="direction"></param>
/// <returns></returns>
public IInputElement? FindNextElement(NavigationDirection direction)
/// <inheritdoc />
public IInputElement? FindNextElement(NavigationDirection direction, FindNextElementOptions? options = null)
{
var xyOption = new XYFocusOptions()
{
UpdateManifold = false
};
ValidateDirection(direction);
return FindNextFocus(direction, xyOption);
var focusOptions = ToFocusOptions(options, false);
var result = FindNextFocus(direction, focusOptions);
_reusableFocusOptions = focusOptions;
return result;
}
/// <summary>
/// Retrieves the element that should receive focus based on the specified navigation direction (cannot be used with tab navigation).
/// </summary>
/// <param name="direction">The direction that focus moves from element to element within the app UI.</param>
/// <param name="options">The options to help identify the next element to receive focus with the provided navigation.</param>
/// <returns>The next element to receive focus.</returns>
public IInputElement? FindNextElement(NavigationDirection direction, FindNextElementOptions options)
private static void ValidateDirection(NavigationDirection direction)
{
return FindNextFocus(direction, ValidateAndCreateFocusOptions(direction, options));
if (direction is not (
NavigationDirection.Next or
NavigationDirection.Previous or
NavigationDirection.Up or
NavigationDirection.Down or
NavigationDirection.Left or
NavigationDirection.Right))
{
throw new ArgumentOutOfRangeException(
nameof(direction),
direction,
$"Only {nameof(NavigationDirection.Next)}, {nameof(NavigationDirection.Previous)}, " +
$"{nameof(NavigationDirection.Up)}, {nameof(NavigationDirection.Down)}," +
$" {nameof(NavigationDirection.Left)} and {nameof(NavigationDirection.Right)} directions are supported");
}
}
private static XYFocusOptions ValidateAndCreateFocusOptions(NavigationDirection direction, FindNextElementOptions options)
private XYFocusOptions ToFocusOptions(FindNextElementOptions? options, bool updateManifold)
{
if (direction is not NavigationDirection.Up
and not NavigationDirection.Down
and not NavigationDirection.Left
and not NavigationDirection.Right)
// XYFocus only uses the options and never modifies them; we can cache and reset them between calls.
var focusOptions = _reusableFocusOptions;
_reusableFocusOptions = null;
if (focusOptions is null)
focusOptions = new XYFocusOptions();
else
focusOptions.Reset();
if (options is not null)
{
throw new ArgumentOutOfRangeException(nameof(direction),
$"{direction} is not supported with FindNextElementOptions. Only Up, Down, Left and right are supported");
focusOptions.SearchRoot = options.SearchRoot;
focusOptions.ExclusionRect = options.ExclusionRect;
focusOptions.FocusHintRectangle = options.FocusHintRectangle;
focusOptions.NavigationStrategyOverride = options.NavigationStrategyOverride;
focusOptions.IgnoreOcclusivity = options.IgnoreOcclusivity;
}
return new XYFocusOptions
{
UpdateManifold = false,
SearchRoot = options.SearchRoot,
ExclusionRect = options.ExclusionRect,
FocusHintRectangle = options.FocusHintRectangle,
NavigationStrategyOverride = options.NavigationStrategyOverride,
IgnoreOcclusivity = options.IgnoreOcclusivity
};
focusOptions.UpdateManifold = updateManifold;
return focusOptions;
}
internal IInputElement? FindNextFocus(NavigationDirection direction, XYFocusOptions focusOptions, bool updateManifolds = true)

24
src/Avalonia.Base/Input/GotFocusEventArgs.cs

@ -1,24 +0,0 @@
using Avalonia.Interactivity;
namespace Avalonia.Input
{
/// <summary>
/// Holds arguments for a <see cref="InputElement.GotFocusEvent"/>.
/// </summary>
public class GotFocusEventArgs : RoutedEventArgs, IKeyModifiersEventArgs
{
public GotFocusEventArgs() : base(InputElement.GotFocusEvent)
{
}
/// <summary>
/// Gets or sets a value indicating how the change in focus occurred.
/// </summary>
public NavigationMethod NavigationMethod { get; init; }
/// <summary>
/// Gets or sets any key modifiers active at the time of focus.
/// </summary>
public KeyModifiers KeyModifiers { get; init; }
}
}

63
src/Avalonia.Base/Input/IFocusManager.cs

@ -14,9 +14,66 @@ namespace Avalonia.Input
IInputElement? GetFocusedElement();
/// <summary>
/// Clears currently focused element.
/// Focuses a control.
/// </summary>
[Unstable("This API might be removed in 11.x minor updates. Please consider focusing another element instead of removing focus at all for better UX.")]
void ClearFocus();
/// <param name="element">The control to focus.</param>
/// <param name="method">The method by which focus was changed.</param>
/// <param name="keyModifiers">Any key modifiers active at the time of focus.</param>
/// <returns><c>true</c> if the focus moved to a control; otherwise, <c>false</c>.</returns>
/// <remarks>
/// If <paramref name="element"/> is null, this method tries to clear the focus. However, it is not advised.
/// For a better user experience, focus should be moved to another element when possible.
///
/// When this method return <c>true</c>, it is not guaranteed that the focus has been moved
/// to <paramref name="element"/>. The focus might have been redirected to another element.
/// </remarks>
bool Focus(
IInputElement? element,
NavigationMethod method = NavigationMethod.Unspecified,
KeyModifiers keyModifiers = KeyModifiers.None);
/// <summary>
/// Attempts to change focus from the element with focus to the next focusable element in the specified direction.
/// </summary>
/// <param name="direction">
/// The direction that focus moves from element to element.
/// Must be one of <see cref="NavigationDirection.Next"/>, <see cref="NavigationDirection.Previous"/>,
/// <see cref="NavigationDirection.Left"/>, <see cref="NavigationDirection.Right"/>,
/// <see cref="NavigationDirection.Up"/> and <see cref="NavigationDirection.Down"/>.
/// </param>
/// <param name="options">
/// The options to help identify the next element to receive focus.
/// They only apply to directional navigation.
/// </param>
/// <returns>true if focus moved; otherwise, false.</returns>
bool TryMoveFocus(NavigationDirection direction, FindNextElementOptions? options = null);
/// <summary>
/// Retrieves the first element that can receive focus.
/// </summary>
/// <returns>The first focusable element.</returns>
IInputElement? FindFirstFocusableElement();
/// <summary>
/// Retrieves the last element that can receive focus.
/// </summary>
/// <returns>The last focusable element.</returns>
IInputElement? FindLastFocusableElement();
/// <summary>
/// Retrieves the element that should receive focus based on the specified navigation direction.
/// </summary>
/// <param name="direction">
/// The direction that focus moves from element to element.
/// Must be one of <see cref="NavigationDirection.Next"/>, <see cref="NavigationDirection.Previous"/>,
/// <see cref="NavigationDirection.Left"/>, <see cref="NavigationDirection.Right"/>,
/// <see cref="NavigationDirection.Up"/> and <see cref="NavigationDirection.Down"/>.
/// </param>
/// <param name="options">
/// The options to help identify the next element to receive focus.
/// They only apply to directional navigation.
/// </param>
/// <returns>The next element to receive focus, if any.</returns>
IInputElement? FindNextElement(NavigationDirection direction, FindNextElementOptions? options = null);
}
}

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

@ -14,12 +14,12 @@ namespace Avalonia.Input
/// <summary>
/// Occurs when the control receives focus.
/// </summary>
event EventHandler<GotFocusEventArgs>? GotFocus;
event EventHandler<FocusChangedEventArgs>? GotFocus;
/// <summary>
/// Occurs when the control loses focus.
/// </summary>
event EventHandler<RoutedEventArgs>? LostFocus;
event EventHandler<FocusChangedEventArgs>? LostFocus;
/// <summary>
/// Occurs when a key is pressed while the control has focus.

30
src/Avalonia.Base/Input/InputElement.Gestures.cs

@ -149,7 +149,7 @@ namespace Avalonia.Input
}
/// <summary>
/// Occurs when a pinch gesture occurs on the control.
/// Occurs when the user moves two contact points closer together.
/// </summary>
public event EventHandler<PinchEventArgs>? Pinch
{
@ -158,7 +158,7 @@ namespace Avalonia.Input
}
/// <summary>
/// Occurs when a pinch gesture ends on the control.
/// Occurs when the user releases both contact points used in a pinch gesture.
/// </summary>
public event EventHandler<PinchEndedEventArgs>? PinchEnded
{
@ -167,7 +167,7 @@ namespace Avalonia.Input
}
/// <summary>
/// Occurs when a pull gesture occurs on the control.
/// Occurs when the user drags from the edge of a control.
/// </summary>
public event EventHandler<PullGestureEventArgs>? PullGesture
{
@ -176,7 +176,7 @@ namespace Avalonia.Input
}
/// <summary>
/// Occurs when a pull gesture ends on the control.
/// Occurs when the user releases the pointer after a pull gesture.
/// </summary>
public event EventHandler<PullGestureEndedEventArgs>? PullGestureEnded
{
@ -185,7 +185,7 @@ namespace Avalonia.Input
}
/// <summary>
/// Occurs when a scroll gesture occurs on the control.
/// Occurs when the user continuously moves the pointer in the same direction within the control’s boundaries.
/// </summary>
public event EventHandler<ScrollGestureEventArgs>? ScrollGesture
{
@ -194,7 +194,7 @@ namespace Avalonia.Input
}
/// <summary>
/// Occurs when a scroll gesture inertia starts on the control.
/// Occurs within a scroll gesture, when the user releases the pointer, and scrolling continues by transitioning to momentum-based gliding movement.
/// </summary>
public event EventHandler<ScrollGestureInertiaStartingEventArgs>? ScrollGestureInertiaStarting
{
@ -203,7 +203,7 @@ namespace Avalonia.Input
}
/// <summary>
/// Occurs when a scroll gesture ends on the control.
/// Occurs when a scroll gesture has fully stopped, taking into account any inertial movement that continues the scroll after the user has released the pointer.
/// </summary>
public event EventHandler<ScrollGestureEndedEventArgs>? ScrollGestureEnded
{
@ -212,7 +212,7 @@ namespace Avalonia.Input
}
/// <summary>
/// Occurs when a touchpad magnify gesture occurs on the control.
/// Occurs when the user moves two contact points away from each other on a touchpad.
/// </summary>
public event EventHandler<PointerDeltaEventArgs>? PointerTouchPadGestureMagnify
{
@ -221,7 +221,7 @@ namespace Avalonia.Input
}
/// <summary>
/// Occurs when a touchpad rotate gesture occurs on the control.
/// Occurs when the user places two contact points and moves them in a circular motion on a touchpad.
/// </summary>
public event EventHandler<PointerDeltaEventArgs>? PointerTouchPadGestureRotate
{
@ -230,7 +230,7 @@ namespace Avalonia.Input
}
/// <summary>
/// Occurs when a swipe gesture occurs on the control.
/// Occurs when the user rapidly drags the pointer in a single direction across the control.
/// </summary>
public event EventHandler<SwipeGestureEventArgs>? SwipeGesture
{
@ -239,7 +239,7 @@ namespace Avalonia.Input
}
/// <summary>
/// Occurs when a touchpad swipe gesture occurs on the control.
/// Occurs when the user performs a rapid dragging motion in a single direction on a touchpad.
/// </summary>
public event EventHandler<PointerDeltaEventArgs>? PointerTouchPadGestureSwipe
{
@ -248,7 +248,7 @@ namespace Avalonia.Input
}
/// <summary>
/// Occurs when a tap gesture occurs on the control.
/// Occurs when the user briefly contacts and releases a single point, without significant movement.
/// </summary>
public event EventHandler<TappedEventArgs>? Tapped
{
@ -257,7 +257,7 @@ namespace Avalonia.Input
}
/// <summary>
/// Occurs when a right tap gesture occurs on the control.
/// Occurs when the user briefly contacts and releases a single point, without significant movement, using a mechanism on the input device recognized as a right button or equivalent.
/// </summary>
public event EventHandler<TappedEventArgs>? RightTapped
{
@ -266,7 +266,7 @@ namespace Avalonia.Input
}
/// <summary>
/// Occurs when a hold gesture occurs on the control.
/// Occurs when the user makes a single contact, then maintains contact beyond a given time threshold without releasing or making another contact.
/// </summary>
public event EventHandler<HoldingRoutedEventArgs>? Holding
{
@ -275,7 +275,7 @@ namespace Avalonia.Input
}
/// <summary>
/// Occurs when a double-tap gesture occurs on the control.
/// Occurs when the user briefly contacts and releases twice on a single point, without significant movement.
/// </summary>
public event EventHandler<TappedEventArgs>? DoubleTapped
{

24
src/Avalonia.Base/Input/InputElement.cs

@ -79,8 +79,8 @@ namespace Avalonia.Input
/// <summary>
/// Defines the <see cref="GotFocus"/> event.
/// </summary>
public static readonly RoutedEvent<GotFocusEventArgs> GotFocusEvent =
RoutedEvent.Register<InputElement, GotFocusEventArgs>(nameof(GotFocus), RoutingStrategies.Bubble);
public static readonly RoutedEvent<FocusChangedEventArgs> GotFocusEvent =
RoutedEvent.Register<InputElement, FocusChangedEventArgs>(nameof(GotFocus), RoutingStrategies.Bubble);
/// <summary>
/// Defines the <see cref="GettingFocus"/> event.
@ -91,8 +91,8 @@ namespace Avalonia.Input
/// <summary>
/// Defines the <see cref="LostFocus"/> event.
/// </summary>
public static readonly RoutedEvent<RoutedEventArgs> LostFocusEvent =
RoutedEvent.Register<InputElement, RoutedEventArgs>(nameof(LostFocus), RoutingStrategies.Bubble);
public static readonly RoutedEvent<FocusChangedEventArgs> LostFocusEvent =
RoutedEvent.Register<InputElement, FocusChangedEventArgs>(nameof(LostFocus), RoutingStrategies.Bubble);
/// <summary>
/// Defines the <see cref="LosingFocus"/> event.
@ -278,7 +278,7 @@ namespace Avalonia.Input
/// <summary>
/// Occurs when the control receives focus.
/// </summary>
public event EventHandler<GotFocusEventArgs>? GotFocus
public event EventHandler<FocusChangedEventArgs>? GotFocus
{
add { AddHandler(GotFocusEvent, value); }
remove { RemoveHandler(GotFocusEvent, value); }
@ -296,7 +296,7 @@ namespace Avalonia.Input
/// <summary>
/// Occurs when the control loses focus.
/// </summary>
public event EventHandler<RoutedEventArgs>? LostFocus
public event EventHandler<FocusChangedEventArgs>? LostFocus
{
add { AddHandler(LostFocusEvent, value); }
remove { RemoveHandler(LostFocusEvent, value); }
@ -523,7 +523,7 @@ namespace Avalonia.Input
if (!IsEffectivelyEnabled && FocusManager.GetFocusManager(this) is { } focusManager
&& Equals(focusManager.GetFocusedElement(), this))
{
focusManager.ClearFocus();
focusManager.Focus(null);
}
}
}
@ -593,7 +593,7 @@ namespace Avalonia.Input
UpdateIsEffectivelyEnabled();
}
private void OnGotFocusCore(GotFocusEventArgs e)
private void OnGotFocusCore(FocusChangedEventArgs e)
{
var isFocused = e.Source == this;
_isFocusVisible = isFocused && (e.NavigationMethod == NavigationMethod.Directional || e.NavigationMethod == NavigationMethod.Tab);
@ -617,11 +617,11 @@ namespace Avalonia.Input
/// for this event.
/// </summary>
/// <param name="e">Data about the event.</param>
protected virtual void OnGotFocus(GotFocusEventArgs e)
protected virtual void OnGotFocus(FocusChangedEventArgs e)
{
}
private void OnLostFocusCore(RoutedEventArgs e)
private void OnLostFocusCore(FocusChangedEventArgs e)
{
_isFocusVisible = false;
IsFocused = false;
@ -634,7 +634,7 @@ namespace Avalonia.Input
/// for this event.
/// </summary>
/// <param name="e">Data about the event.</param>
protected virtual void OnLostFocus(RoutedEventArgs e)
protected virtual void OnLostFocus(FocusChangedEventArgs e)
{
}
@ -995,7 +995,7 @@ namespace Avalonia.Input
}
else
{
focusManager.ClearFocus();
focusManager.Focus(null);
}
}
}

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

@ -167,7 +167,7 @@ namespace Avalonia.Input
if (s_keySynonyms.TryGetValue(keyStr.ToLower(CultureInfo.InvariantCulture), out key))
return true;
if (EnumHelper.TryParse(keyStr, true, out key))
if (Enum.TryParse(keyStr, true, out key))
return true;
return false;
@ -187,7 +187,7 @@ namespace Avalonia.Input
return KeyModifiers.Meta;
}
return EnumHelper.Parse<KeyModifiers>(modifier.ToString(), true);
return Enum.Parse<KeyModifiers>(modifier.ToString(), true);
}
private static Key ResolveNumPadOperationKey(Key key)

22
src/Avalonia.Base/Input/KeyboardDevice.cs

@ -188,25 +188,35 @@ namespace Avalonia.Input
if (changeFocus)
{
var oldElement = FocusedElement;
// Clear keyboard focus from currently focused element
if (FocusedElement != null &&
(!((Visual)FocusedElement).IsAttachedToVisualTree ||
if (oldElement != null &&
(!((Visual)oldElement).IsAttachedToVisualTree ||
_focusedRoot != ((Visual?)element)?.GetInputRoot()) &&
_focusedRoot != null)
{
ClearChildrenFocusWithin(_focusedRoot.RootElement, true);
}
SetIsFocusWithin(FocusedElement, element);
SetIsFocusWithin(oldElement, element);
_focusedElement = element;
_focusedRoot = (_focusedElement as Visual)?.GetInputRoot();
interactive?.RaiseEvent(new RoutedEventArgs(InputElement.LostFocusEvent));
interactive?.RaiseEvent(new FocusChangedEventArgs(InputElement.LostFocusEvent)
{
OldFocusedElement = oldElement,
NewFocusedElement = element,
NavigationMethod = method,
KeyModifiers = keyModifiers
});
(element as Interactive)?.RaiseEvent(new GotFocusEventArgs
(element as Interactive)?.RaiseEvent(new FocusChangedEventArgs(InputElement.GotFocusEvent)
{
OldFocusedElement = oldElement,
NewFocusedElement = element,
NavigationMethod = method,
KeyModifiers = keyModifiers,
KeyModifiers = keyModifiers
});
_textInputManager.SetFocusedElement(element);

29
src/Avalonia.Base/Input/Navigation/XYFocusOptions.cs

@ -1,17 +1,38 @@
namespace Avalonia.Input.Navigation;
internal class XYFocusOptions
internal sealed class XYFocusOptions
{
public InputElement? SearchRoot { get; set; }
public Rect ExclusionRect { get; set; }
public Rect? FocusHintRectangle { get; set; }
public Rect? FocusedElementBounds { get; set; }
public XYFocusNavigationStrategy? NavigationStrategyOverride { get; set; }
public bool IgnoreClipping { get; set; } = true;
public bool IgnoreClipping { get; set; }
public bool IgnoreCone { get; set; }
public KeyDeviceType? KeyDeviceType { get; set; }
public bool ConsiderEngagement { get; set; } = true;
public bool UpdateManifold { get; set; } = true;
public bool ConsiderEngagement { get; set; }
public bool UpdateManifold { get; set; }
public bool UpdateManifoldsFromFocusHintRect { get; set; }
public bool IgnoreOcclusivity { get; set; }
public XYFocusOptions()
{
Reset();
}
internal void Reset()
{
SearchRoot = null;
ExclusionRect = default;
FocusHintRectangle = null;
FocusedElementBounds = null;
NavigationStrategyOverride = null;
IgnoreClipping = true;
IgnoreCone = false;
KeyDeviceType = null;
ConsiderEngagement = true;
UpdateManifold = true;
UpdateManifoldsFromFocusHintRect = false;
IgnoreOcclusivity = false;
}
}

5
src/Avalonia.Base/Layout/LayoutHelper.cs

@ -263,12 +263,7 @@ namespace Avalonia.Layout
// point precision error (e.g. 79.333333333333343) then when it's multiplied by
// `dpiScale` and rounded up, it will be rounded up to a value one greater than it
// should be.
#if NET6_0_OR_GREATER
return Math.Round(value, 8, MidpointRounding.ToZero);
#else
// MidpointRounding.ToZero isn't available in netstandard2.0.
return Math.Truncate(value * 1e8) / 1e8;
#endif
}
}
}

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

@ -43,11 +43,7 @@ namespace Avalonia.Media.Fonts
}
private static string[] SplitNames(string names)
#if NET6_0_OR_GREATER
=> names.Split(',', StringSplitOptions.TrimEntries);
#else
=> Array.ConvertAll(names.Split(','), p => p.Trim());
#endif
/// <summary>
/// Gets the primary family name.

45
src/Avalonia.Base/Media/Fonts/FontCollectionBase.cs

@ -168,7 +168,7 @@ namespace Avalonia.Media.Fonts
var key = typeface.ToFontCollectionKey();
return TryGetGlyphTypeface(familyName, key, out glyphTypeface);
return TryGetGlyphTypeface(familyName, key, allowNearestMatch: true, out glyphTypeface);
}
public virtual bool TryGetFamilyTypefaces(string familyName, [NotNullWhen(true)] out IReadOnlyList<Typeface>? familyTypefaces)
@ -455,25 +455,25 @@ namespace Avalonia.Media.Fonts
/// find the best match based on the provided <paramref name="key"/>.</remarks>
/// <param name="familyName">The name of the font family to search for. This parameter is case-insensitive.</param>
/// <param name="key">The key representing the desired font collection attributes.</param>
/// <param name="allowNearestMatch">Whether to allow a nearest match (as opposed to only an exact match).</param>
/// <param name="glyphTypeface">When this method returns, contains the matching <see cref="GlyphTypeface"/> if a match is found; otherwise,
/// <see langword="null"/>.</param>
/// <returns><see langword="true"/> if a matching glyph typeface is found; otherwise, <see langword="false"/>.</returns>
protected bool TryGetGlyphTypeface(string familyName, FontCollectionKey key, [NotNullWhen(true)] out GlyphTypeface? glyphTypeface)
protected bool TryGetGlyphTypeface(
string familyName,
FontCollectionKey key,
bool allowNearestMatch,
[NotNullWhen(true)] out GlyphTypeface? glyphTypeface)
{
glyphTypeface = null;
if (_glyphTypefaceCache.TryGetValue(familyName, out var glyphTypefaces))
{
if (glyphTypefaces.TryGetValue(key, out glyphTypeface) && glyphTypeface != null)
{
return true;
}
if (TryGetNearestMatch(glyphTypefaces, key, out glyphTypeface))
if (TryGetMatch(glyphTypefaces, key, allowNearestMatch, out glyphTypeface, out var isNearestMatch))
{
var matchedKey = glyphTypeface.ToFontCollectionKey();
if (matchedKey != key)
if (isNearestMatch && matchedKey != key)
{
if (TryCreateSyntheticGlyphTypeface(glyphTypeface, key.Style, key.Weight, key.Stretch, out var syntheticGlyphTypeface))
{
@ -511,7 +511,7 @@ namespace Avalonia.Media.Fonts
{
// Exact match found in snapshot. Use the exact family name for lookup
if (_glyphTypefaceCache.TryGetValue(snapshot[mid].Name, out var exactGlyphTypefaces) &&
TryGetNearestMatch(exactGlyphTypefaces, key, out glyphTypeface))
TryGetMatch(exactGlyphTypefaces, key, allowNearestMatch, out glyphTypeface, out _))
{
return true;
}
@ -549,7 +549,7 @@ namespace Avalonia.Media.Fonts
}
if (_glyphTypefaceCache.TryGetValue(fontFamily.Name, out glyphTypefaces) &&
TryGetNearestMatch(glyphTypefaces, key, out glyphTypeface))
TryGetMatch(glyphTypefaces, key, allowNearestMatch, out glyphTypeface, out _))
{
return true;
}
@ -559,6 +559,29 @@ namespace Avalonia.Media.Fonts
return false;
}
private bool TryGetMatch(
IDictionary<FontCollectionKey, GlyphTypeface?> glyphTypefaces,
FontCollectionKey key,
bool allowNearestMatch,
[NotNullWhen(true)] out GlyphTypeface? glyphTypeface,
out bool isNearestMatch)
{
if (glyphTypefaces.TryGetValue(key, out glyphTypeface) && glyphTypeface is not null)
{
isNearestMatch = false;
return true;
}
if (allowNearestMatch && TryGetNearestMatch(glyphTypefaces, key, out glyphTypeface))
{
isNearestMatch = true;
return true;
}
isNearestMatch = false;
return false;
}
/// <summary>
/// Attempts to retrieve the nearest matching <see cref="GlyphTypeface"/> for the specified font key from the
/// provided collection of glyph typefaces.

15
src/Avalonia.Base/Media/Fonts/SystemFontCollection.cs

@ -29,14 +29,14 @@ namespace Avalonia.Media.Fonts
FontStretch stretch, [NotNullWhen(true)] out GlyphTypeface? glyphTypeface)
{
var typeface = new Typeface(familyName, style, weight, stretch).Normalize(out familyName);
var key = typeface.ToFontCollectionKey();
if (base.TryGetGlyphTypeface(familyName, style, weight, stretch, out glyphTypeface))
// Find an exact match first
if (TryGetGlyphTypeface(familyName, key, allowNearestMatch: false, out glyphTypeface))
{
return true;
}
var key = typeface.ToFontCollectionKey();
//Check cache first to avoid unnecessary calls to the font manager
if (_glyphTypefaceCache.TryGetValue(familyName, out var glyphTypefaces) && glyphTypefaces.TryGetValue(key, out glyphTypeface))
{
@ -52,6 +52,13 @@ namespace Avalonia.Media.Fonts
return false;
}
// The font manager didn't return a perfect match either. Find the nearest match ourselves.
if (key != platformTypeface.ToFontCollectionKey() &&
TryGetGlyphTypeface(familyName, key, allowNearestMatch: true, out glyphTypeface))
{
return true;
}
glyphTypeface = GlyphTypeface.TryCreate(platformTypeface);
if (glyphTypeface is null)
{
@ -77,7 +84,7 @@ namespace Avalonia.Media.Fonts
}
//Requested glyph typeface should be in cache now
return base.TryGetGlyphTypeface(familyName, style, weight, stretch, out glyphTypeface);
return TryGetGlyphTypeface(familyName, key, allowNearestMatch: false, out glyphTypeface);
}
public override bool TryGetFamilyTypefaces(string familyName, [NotNullWhen(true)] out IReadOnlyList<Typeface>? familyTypefaces)

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

@ -126,12 +126,10 @@ namespace Avalonia.Media
return array.AsSpan();
}
#if NET6_0_OR_GREATER
if (list is List<ushort> concreteList)
{
return CollectionsMarshal.AsSpan(concreteList);
}
#endif
array = new ushort[count];
for (var i = 0; i < count; ++i)

66
src/Avalonia.Base/Media/TextFormatting/FormattedTextSource.cs

@ -132,5 +132,71 @@ namespace Avalonia.Media.TextFormatting
return Math.Min(length, text.Length);
}
/// <summary>
/// References a portion of a text buffer.
/// </summary>
private readonly record struct TextRange
{
public TextRange(int start, int length)
{
Start = start;
Length = length;
}
/// <summary>
/// Gets the start.
/// </summary>
/// <value>
/// The start.
/// </value>
public int Start { get; }
/// <summary>
/// Gets the length.
/// </summary>
/// <value>
/// The length.
/// </value>
public int Length { get; }
/// <summary>
/// Gets the end.
/// </summary>
/// <value>
/// The end.
/// </value>
public int End => Start + Length - 1;
/// <summary>
/// Returns a specified number of contiguous elements from the start of the slice.
/// </summary>
/// <param name="length">The number of elements to return.</param>
/// <returns>A <see cref="TextRange"/> that contains the specified number of elements from the start of this slice.</returns>
public TextRange Take(int length)
{
if (length > Length)
{
throw new ArgumentOutOfRangeException(nameof(length));
}
return new TextRange(Start, length);
}
/// <summary>
/// Bypasses a specified number of elements in the slice and then returns the remaining elements.
/// </summary>
/// <param name="length">The number of elements to skip before returning the remaining elements.</param>
/// <returns>A <see cref="TextRange"/> that contains the elements that occur after the specified index in this slice.</returns>
public TextRange Skip(int length)
{
if (length > Length)
{
throw new ArgumentOutOfRangeException(nameof(length));
}
return new TextRange(Start + length, Length - length);
}
}
}
}

15
src/Avalonia.Base/Media/TextFormatting/FormattingBufferHelper.cs

@ -52,11 +52,7 @@ namespace Avalonia.Media.TextFormatting
// dictionary is in fact larger than that: it has entries and buckets, but let's only count our data here
if (IsBufferTooLarge<KeyValuePair<TKey, TValue>>(approximateCapacity))
{
#if NET6_0_OR_GREATER
dictionary.TrimExcess();
#else
dictionary = new Dictionary<TKey, TValue>();
#endif
}
}
@ -67,18 +63,7 @@ namespace Avalonia.Media.TextFormatting
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static uint RoundUpToPowerOf2(uint value)
{
#if NET6_0_OR_GREATER
return BitOperations.RoundUpToPowerOf2(value);
#else
// Based on https://graphics.stanford.edu/~seander/bithacks.html#RoundUpPowerOf2
--value;
value |= value >> 1;
value |= value >> 2;
value |= value >> 4;
value |= value >> 8;
value |= value >> 16;
return value + 1;
#endif
}
}
}

70
src/Avalonia.Base/Media/TextFormatting/TextRange.cs

@ -1,70 +0,0 @@
using System;
namespace Avalonia.Media.TextFormatting
{
/// <summary>
/// References a portion of a text buffer.
/// </summary>
public readonly record struct TextRange
{
public TextRange(int start, int length)
{
Start = start;
Length = length;
}
/// <summary>
/// Gets the start.
/// </summary>
/// <value>
/// The start.
/// </value>
public int Start { get; }
/// <summary>
/// Gets the length.
/// </summary>
/// <value>
/// The length.
/// </value>
public int Length { get; }
/// <summary>
/// Gets the end.
/// </summary>
/// <value>
/// The end.
/// </value>
public int End => Start + Length - 1;
/// <summary>
/// Returns a specified number of contiguous elements from the start of the slice.
/// </summary>
/// <param name="length">The number of elements to return.</param>
/// <returns>A <see cref="TextRange"/> that contains the specified number of elements from the start of this slice.</returns>
public TextRange Take(int length)
{
if (length > Length)
{
throw new ArgumentOutOfRangeException(nameof(length));
}
return new TextRange(Start, length);
}
/// <summary>
/// Bypasses a specified number of elements in the slice and then returns the remaining elements.
/// </summary>
/// <param name="length">The number of elements to skip before returning the remaining elements.</param>
/// <returns>A <see cref="TextRange"/> that contains the elements that occur after the specified index in this slice.</returns>
public TextRange Skip(int length)
{
if (length > Length)
{
throw new ArgumentOutOfRangeException(nameof(length));
}
return new TextRange(Start + length, Length - length);
}
}
}

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

@ -174,17 +174,17 @@ namespace Avalonia.Media
// Try match with font style, weight or stretch and update accordingly.
var match = false;
if (EnumHelper.TryParse<FontStyle>(token, true, out var newStyle))
if (Enum.TryParse<FontStyle>(token, true, out var newStyle))
{
style = newStyle;
match = true;
}
else if (EnumHelper.TryParse<FontWeight>(token, true, out var newWeight))
else if (Enum.TryParse<FontWeight>(token, true, out var newWeight))
{
weight = newWeight;
match = true;
}
else if (EnumHelper.TryParse<FontStretch>(token, true, out var newStretch))
else if (Enum.TryParse<FontStretch>(token, true, out var newStretch))
{
stretch = newStretch;
match = true;

13
src/Avalonia.Base/Platform/Internal/UnmanagedBlob.cs

@ -3,7 +3,6 @@ using System.Collections.Generic;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using System.Threading;
using Avalonia.Compatibility;
namespace Avalonia.Platform.Internal;
@ -117,7 +116,7 @@ internal class UnmanagedBlob : IDisposable
// Could be replaced with https://github.com/dotnet/runtime/issues/40892 when it will be available.
private IntPtr Alloc(int size)
{
if (!OperatingSystemEx.IsLinux())
if (!OperatingSystem.IsLinux())
{
return Marshal.AllocHGlobal(size);
}
@ -126,12 +125,8 @@ internal class UnmanagedBlob : IDisposable
var rv = mmap(IntPtr.Zero, new IntPtr(size), 3, 0x22, -1, IntPtr.Zero);
if (rv.ToInt64() == -1 || (ulong)rv.ToInt64() == 0xffffffff)
{
#if NET6_0_OR_GREATER
var errno = Marshal.GetLastSystemError();
throw new Exception("Unable to allocate memory: " + errno);
#else
throw new Exception("Unable to allocate memory");
#endif
}
return rv;
}
@ -139,7 +134,7 @@ internal class UnmanagedBlob : IDisposable
private void Free(IntPtr ptr, int len)
{
if (!OperatingSystemEx.IsLinux())
if (!OperatingSystem.IsLinux())
{
Marshal.FreeHGlobal(ptr);
}
@ -147,12 +142,8 @@ internal class UnmanagedBlob : IDisposable
{
if (munmap(ptr, new IntPtr(len)) == -1)
{
#if NET6_0_OR_GREATER
var errno = Marshal.GetLastSystemError();
throw new Exception("Unable to free memory: " + errno);
#else
throw new Exception("Unable to free memory");
#endif
}
}
}

12
src/Avalonia.Base/Platform/StandardRuntimePlatform.cs

@ -1,4 +1,4 @@
using Avalonia.Compatibility;
using System;
using Avalonia.Metadata;
namespace Avalonia.Platform
@ -8,11 +8,11 @@ namespace Avalonia.Platform
{
public virtual RuntimePlatformInfo GetRuntimeInfo() => new()
{
IsDesktop = OperatingSystemEx.IsWindows()
|| OperatingSystemEx.IsMacOS() || OperatingSystemEx.IsMacCatalyst()
|| OperatingSystemEx.IsLinux() || OperatingSystemEx.IsFreeBSD(),
IsMobile = OperatingSystemEx.IsAndroid() || (OperatingSystemEx.IsIOS() && !OperatingSystemEx.IsMacCatalyst()),
IsTV = OperatingSystemEx.IsTvOS()
IsDesktop = OperatingSystem.IsWindows()
|| OperatingSystem.IsMacOS() || OperatingSystem.IsMacCatalyst()
|| OperatingSystem.IsLinux() || OperatingSystem.IsFreeBSD(),
IsMobile = OperatingSystem.IsAndroid() || (OperatingSystem.IsIOS() && !OperatingSystem.IsMacCatalyst()),
IsTV = OperatingSystem.IsTvOS()
};
}
}

3
src/Avalonia.Base/Platform/StandardRuntimePlatformServices.cs

@ -1,7 +1,4 @@
using System.Reflection;
using Avalonia.Compatibility;
using Avalonia.Platform.Internal;
using Avalonia.Platform.Interop;
namespace Avalonia.Platform;

11
src/Avalonia.Base/Platform/Storage/FileIO/BclLauncher.cs

@ -2,7 +2,6 @@
using System.Diagnostics;
using System.Text.RegularExpressions;
using System.Threading.Tasks;
using Avalonia.Compatibility;
namespace Avalonia.Platform.Storage.FileIO;
@ -39,7 +38,7 @@ internal class BclLauncher : ILauncher
private static bool Exec(string urlOrFile)
{
if (OperatingSystemEx.IsLinux())
if (OperatingSystem.IsLinux())
{
// If no associated application/json MimeType is found xdg-open opens return error
// but it tries to open it anyway using the console editor (nano, vim, other..)
@ -47,17 +46,17 @@ internal class BclLauncher : ILauncher
ShellExecRaw($"xdg-open \\\"{args}\\\"", waitForExit: false);
return true;
}
else if (OperatingSystemEx.IsWindows() || OperatingSystemEx.IsMacOS())
else if (OperatingSystem.IsWindows() || OperatingSystem.IsMacOS())
{
var info = new ProcessStartInfo
{
FileName = OperatingSystemEx.IsWindows() ? urlOrFile : "open",
FileName = OperatingSystem.IsWindows() ? urlOrFile : "open",
CreateNoWindow = true,
UseShellExecute = OperatingSystemEx.IsWindows()
UseShellExecute = OperatingSystem.IsWindows()
};
// Using the argument list avoids having to escape spaces and other special
// characters that are part of valid macos file and folder paths.
if (OperatingSystemEx.IsMacOS())
if (OperatingSystem.IsMacOS())
info.ArgumentList.Add(urlOrFile);
using var process = Process.Start(info);
return true;

7
src/Avalonia.Base/Platform/Storage/FileIO/BclStorageProvider.cs

@ -3,7 +3,6 @@ using System.Collections.Generic;
using System.IO;
using System.Runtime.InteropServices;
using System.Threading.Tasks;
using Avalonia.Compatibility;
using Avalonia.Logging;
namespace Avalonia.Platform.Storage.FileIO;
@ -107,13 +106,13 @@ internal abstract class BclStorageProvider : IStorageProvider
// Normally we want to avoid platform specific code in the Avalonia.Base assembly.
protected static string? GetDownloadsWellKnownFolder()
{
if (OperatingSystemEx.IsWindows())
if (OperatingSystem.IsWindows())
{
return Environment.OSVersion.Version.Major < 6 ? null :
TryGetWindowsKnownFolder(s_folderDownloads);
}
if (OperatingSystemEx.IsLinux())
if (OperatingSystem.IsLinux())
{
var envDir = Environment.GetEnvironmentVariable("XDG_DOWNLOAD_DIR");
if (envDir != null && Directory.Exists(envDir))
@ -122,7 +121,7 @@ internal abstract class BclStorageProvider : IStorageProvider
}
}
if (OperatingSystemEx.IsLinux() || OperatingSystemEx.IsMacOS())
if (OperatingSystem.IsLinux() || OperatingSystem.IsMacOS())
{
return "~/Downloads";
}

8
src/Avalonia.Base/Platform/Storage/FileIO/SecurityScopedStream.cs

@ -40,12 +40,10 @@ internal sealed class SecurityScopedStream(FileStream _stream, IDisposable _secu
public override Task<int> ReadAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken) =>
_stream.ReadAsync(buffer, offset, count, cancellationToken);
#if NET6_0_OR_GREATER
public override int Read(Span<byte> buffer) => _stream.Read(buffer);
public override ValueTask<int> ReadAsync(Memory<byte> buffer, CancellationToken cancellationToken = default) =>
_stream.ReadAsync(buffer, cancellationToken);
#endif
public override void Write(byte[] buffer, int offset, int count) =>
_stream.Write(buffer, offset, count);
@ -53,12 +51,10 @@ internal sealed class SecurityScopedStream(FileStream _stream, IDisposable _secu
public override Task WriteAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken) =>
_stream.WriteAsync(buffer, offset, count, cancellationToken);
#if NET6_0_OR_GREATER
public override void Write(ReadOnlySpan<byte> buffer) => _stream.Write(buffer);
public override ValueTask WriteAsync(ReadOnlyMemory<byte> buffer, CancellationToken cancellationToken = default) =>
_stream.WriteAsync(buffer, cancellationToken);
#endif
public override void WriteByte(byte value) => _stream.WriteByte(value);
@ -68,9 +64,7 @@ internal sealed class SecurityScopedStream(FileStream _stream, IDisposable _secu
public override void SetLength(long value) =>
_stream.SetLength(value);
#if NET6_0_OR_GREATER
public override void CopyTo(Stream destination, int bufferSize) => _stream.CopyTo(destination, bufferSize);
#endif
public override Task CopyToAsync(Stream destination, int bufferSize, CancellationToken cancellationToken) =>
_stream.CopyToAsync(destination, bufferSize, cancellationToken);
@ -100,7 +94,6 @@ internal sealed class SecurityScopedStream(FileStream _stream, IDisposable _secu
}
}
#if NET6_0_OR_GREATER
public override async ValueTask DisposeAsync()
{
try
@ -112,5 +105,4 @@ internal sealed class SecurityScopedStream(FileStream _stream, IDisposable _secu
_securityScope.Dispose();
}
}
#endif
}

12
src/Avalonia.Base/Platform/Storage/FileIO/StorageBookmarkHelper.cs

@ -58,11 +58,7 @@ internal static class StorageBookmarkHelper
nativeBookmarkBytes.CopyTo(arraySpan.Slice(HeaderLength));
// We must use span overload because ArrayPool might return way too big array.
#if NET6_0_OR_GREATER
return Convert.ToBase64String(arraySpan);
#else
return Convert.ToBase64String(arraySpan.ToArray(), Base64FormattingOptions.None);
#endif
}
finally
{
@ -89,7 +85,7 @@ internal static class StorageBookmarkHelper
}
Span<byte> decodedBookmark;
#if NET6_0_OR_GREATER
// Each base64 character represents 6 bits, but to be safe,
var arrayPool = ArrayPool<byte>.Shared.Rent(HeaderLength + base64bookmark.Length * 6);
if (Convert.TryFromBase64Chars(base64bookmark, arrayPool, out int bytesWritten))
@ -101,9 +97,7 @@ internal static class StorageBookmarkHelper
nativeBookmark = null;
return DecodeResult.InvalidFormat;
}
#else
decodedBookmark = Convert.FromBase64String(base64bookmark).AsSpan();
#endif
try
{
if (decodedBookmark.Length < HeaderLength
@ -126,9 +120,7 @@ internal static class StorageBookmarkHelper
}
finally
{
#if NET6_0_OR_GREATER
ArrayPool<byte>.Shared.Return(arrayPool);
#endif
}
}

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

@ -51,7 +51,7 @@ namespace Avalonia.Rendering.Composition
/// </summary>
[PrivateApi]
public Compositor(IPlatformGraphics? gpu, bool useUiThreadForSynchronousCommits = false)
: this(RenderLoop.LocatorAutoInstance, gpu, useUiThreadForSynchronousCommits)
: this(AvaloniaLocator.Current.GetRequiredService<IRenderLoop>(), gpu, useUiThreadForSynchronousCommits)
{
}

4
src/Avalonia.Base/Rendering/Composition/Server/FpsCounter.cs

@ -42,11 +42,7 @@ internal class FpsCounter
_lastFpsUpdate = now;
}
#if NET6_0_OR_GREATER
var fpsLine = string.Create(CultureInfo.InvariantCulture, $"Frame #{_totalFrames:00000000} FPS: {_fps:000} {aux}");
#else
var fpsLine = FormattableString.Invariant($"Frame #{_totalFrames:00000000} FPS: {_fps:000} {aux}");
#endif
var size = _textRenderer.MeasureAsciiText(fpsLine.AsSpan());

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

@ -104,14 +104,9 @@ internal sealed class FrameTimeGraph
var brush = value <= _defaultMaxY ? Brushes.Black : Brushes.Red;
#if NET6_0_OR_GREATER
Span<char> buffer = stackalloc char[24];
buffer.TryWrite(CultureInfo.InvariantCulture, $"{label}: {value,5:F2}ms", out var charsWritten);
_textRenderer.DrawAsciiText(context, buffer.Slice(0, charsWritten), brush);
#else
var text = FormattableString.Invariant($"{label}: {value,5:F2}ms");
_textRenderer.DrawAsciiText(context, text.AsSpan(), brush);
#endif
}
private IStreamGeometryImpl BuildGraphGeometry(double maxY)

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

@ -39,6 +39,11 @@ namespace Avalonia.Rendering.Composition.Server
public ICompositionTargetDebugEvents? DebugEvents { get; set; }
public int RenderedVisuals { get; set; }
public int VisitedVisuals { get; set; }
/// <summary>
/// Returns true if the target is enabled and has pending work but its render target was not ready.
/// </summary>
internal bool IsWaitingForReadyRenderTarget { get; private set; }
public ServerCompositionTarget(ServerCompositor compositor, Func<IEnumerable<IPlatformRenderSurface>> surfaces)
: base(compositor)
@ -125,6 +130,8 @@ namespace Avalonia.Rendering.Composition.Server
public void Render()
{
IsWaitingForReadyRenderTarget = false;
if (_disposed)
return;
@ -143,11 +150,15 @@ namespace Avalonia.Rendering.Composition.Server
try
{
if (_renderTarget == null && !_compositor.IsReadyToCreateRenderTarget(_surfaces()))
{
IsWaitingForReadyRenderTarget = IsEnabled;
return;
}
_renderTarget ??= _compositor.CreateRenderTarget(_surfaces());
}
catch (RenderTargetNotReadyException)
{
IsWaitingForReadyRenderTarget = IsEnabled;
return;
}
catch (RenderTargetCorruptedException)
@ -164,7 +175,10 @@ namespace Avalonia.Rendering.Composition.Server
return;
if (!_renderTarget.IsReady)
{
IsWaitingForReadyRenderTarget = IsEnabled;
return;
}
var needLayer = _overlays.RequireLayer // Check if we don't need overlays
// Check if render target can be rendered to directly and preserves the previous frame

6
src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionVisual/ServerCompositionVisual.Adorners.cs

@ -52,7 +52,7 @@ partial class ServerCompositionVisual
// We ignore Visual's RenderTransform completely since it's set by AdornerLayer and can be out of sync
// with compositor-driver animations
var ownTransform = MatrixUtils.ComputeTransform(Size, AnchorPoint, CenterPoint, Matrix.Identity, Scale,
RotationAngle, Orientation, Offset);
RotationAngle, Orientation, Offset + Translation);
if (
AdornerLayer_GetExpectedSharedAncestor(this) is {} sharedAncestor
&& ComputeTransformFromAncestor(AdornedVisual, sharedAncestor, out var adornerLayerToAdornedVisual))
@ -63,7 +63,7 @@ partial class ServerCompositionVisual
}
else
_ownTransform = MatrixUtils.ComputeTransform(Size, AnchorPoint, CenterPoint, TransformMatrix, Scale,
RotationAngle, Orientation, Offset);
RotationAngle, Orientation, Offset + Translation);
PropagateFlags(true, true);
@ -170,4 +170,4 @@ partial class ServerCompositionVisual
_pools.IntStackPool.Return(ref _adornerPushedClipStack!);
}
}
}
}

8
src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionVisual/ServerCompositionVisual.ComputedProperties.cs

@ -50,7 +50,7 @@ partial class ServerCompositionVisual
private LtrbRect? _ownClipRect;
private bool _hasExtraDirtyRect;
private bool _needsToAddExtraDirtyRectToDirtyRegion;
private LtrbRect _extraDirtyRect;
public virtual LtrbRect? ComputeOwnContentBounds() => null;
@ -107,7 +107,7 @@ partial class ServerCompositionVisual
_isDirtyForRender |= dirtyForRender;
// If node itself is dirty for render, we don't need to keep track of extra dirty rects
_hasExtraDirtyRect = !dirtyForRender && (_hasExtraDirtyRect || additionalDirtyRegion);
_needsToAddExtraDirtyRectToDirtyRegion = !dirtyForRender && (_needsToAddExtraDirtyRectToDirtyRegion || additionalDirtyRegion);
}
public void RecomputeOwnProperties()
@ -148,7 +148,7 @@ partial class ServerCompositionVisual
if (_combinedTransformDirty)
{
_ownTransform = MatrixUtils.ComputeTransform(Size, AnchorPoint, CenterPoint, TransformMatrix, Scale,
RotationAngle, Orientation, Offset);
RotationAngle, Orientation, Offset + Translation);
setDirtyForRender = setDirtyBounds = true;
@ -161,4 +161,4 @@ partial class ServerCompositionVisual
_ownBoundsDirty = _clipSizeDirty = _combinedTransformDirty = _compositionFieldsDirty = false;
PropagateFlags(setDirtyBounds, setDirtyForRender, setHasExtraDirtyRect);
}
}
}

9
src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionVisual/ServerCompositionVisual.DirtyInputs.cs

@ -43,7 +43,9 @@ partial class ServerCompositionVisual
| CompositionVisualChangedFields.Orientation
| CompositionVisualChangedFields.OrientationAnimated
| CompositionVisualChangedFields.Offset
| CompositionVisualChangedFields.OffsetAnimated;
| CompositionVisualChangedFields.OffsetAnimated
| CompositionVisualChangedFields.Translation
| CompositionVisualChangedFields.TranslationAnimated;
private const CompositionVisualChangedFields ClipSizeDirtyMask =
CompositionVisualChangedFields.Size
@ -100,7 +102,8 @@ partial class ServerCompositionVisual
|| property == s_IdOfScaleProperty
|| property == s_IdOfRotationAngleProperty
|| property == s_IdOfOrientationProperty
|| property == s_IdOfOffsetProperty)
|| property == s_IdOfOffsetProperty
|| property == s_IdOfTranslationProperty)
TriggerCombinedTransformDirty();
if (property == s_IdOfClipToBoundsProperty
@ -163,7 +166,7 @@ partial class ServerCompositionVisual
protected void AddExtraDirtyRect(LtrbRect rect)
{
_extraDirtyRect = _hasExtraDirtyRect ? _extraDirtyRect.Union(rect) : rect;
_extraDirtyRect = _delayPropagateHasExtraDirtyRects ? _extraDirtyRect.Union(rect) : rect;
_delayPropagateHasExtraDirtyRects = true;
EnqueueOwnPropertiesRecompute();
}

6
src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionVisual/ServerCompositionVisual.Update.cs

@ -56,7 +56,7 @@ internal partial class ServerCompositionVisual
private bool NeedToPushBoundsAffectingProperties(ServerCompositionVisual node)
{
return (node._isDirtyForRenderInSubgraph || node._hasExtraDirtyRect || node._contentChanged);
return (node._isDirtyForRenderInSubgraph || node._needsToAddExtraDirtyRectToDirtyRegion || node._contentChanged);
}
public void PreSubgraph(ServerCompositionVisual node, out bool visitChildren)
@ -142,7 +142,7 @@ internal partial class ServerCompositionVisual
// specified before the tranform, i.e. in inner space, hence we have to pick them
// up before we pop the transform from the transform stack.
//
if (node._hasExtraDirtyRect)
if (node._needsToAddExtraDirtyRectToDirtyRegion)
{
AddToDirtyRegion(node._extraDirtyRect);
}
@ -169,7 +169,7 @@ internal partial class ServerCompositionVisual
node._isDirtyForRender = false;
node._isDirtyForRenderInSubgraph = false;
node._needsBoundingBoxUpdate = false;
node._hasExtraDirtyRect = false;
node._needsToAddExtraDirtyRectToDirtyRegion = false;
node._contentChanged = false;
}

44
src/Avalonia.Base/Rendering/Composition/Server/ServerCompositor.cs

@ -44,6 +44,9 @@ namespace Avalonia.Rendering.Composition.Server
public CompositionOptions Options { get; }
public ServerCompositorAnimations Animations { get; }
public ReadbackIndices Readback { get; } = new();
private int _ticksSinceLastCommit;
private const int CommitGraceTicks = 10;
public ServerCompositor(IRenderLoop renderLoop, IPlatformGraphics? platformGraphics,
CompositionOptions options,
@ -64,6 +67,7 @@ namespace Avalonia.Rendering.Composition.Server
{
lock (_batches)
_batches.Enqueue(batch);
_renderLoop.Wakeup();
}
internal void UpdateServerTime() => ServerNow = Clock.Elapsed;
@ -72,6 +76,7 @@ namespace Avalonia.Rendering.Composition.Server
readonly List<CompositionBatch> _reusableToNotifyRenderedList = new();
void ApplyPendingBatches()
{
bool hadBatches = false;
while (true)
{
CompositionBatch batch;
@ -119,7 +124,13 @@ namespace Avalonia.Rendering.Composition.Server
_reusableToNotifyProcessedList.Add(batch);
LastBatchId = batch.SequenceId;
hadBatches = true;
}
if (hadBatches)
_ticksSinceLastCommit = 0;
else if (_ticksSinceLastCommit < int.MaxValue)
_ticksSinceLastCommit++;
}
void ReadServerJobs(BatchStreamReader reader, Queue<Action> queue, object endMarker)
@ -171,8 +182,10 @@ namespace Avalonia.Rendering.Composition.Server
_reusableToNotifyRenderedList.Clear();
}
public void Render() => Render(true);
public void Render(bool catchExceptions)
bool IRenderLoopTask.Render() => ExecuteRender(true);
public void Render(bool catchExceptions) => ExecuteRender(catchExceptions);
private bool ExecuteRender(bool catchExceptions)
{
if (Dispatcher.UIThread.CheckAccess())
{
@ -182,7 +195,7 @@ namespace Avalonia.Rendering.Composition.Server
try
{
using (Dispatcher.UIThread.DisableProcessing())
RenderReentrancySafe(catchExceptions);
return RenderReentrancySafe(catchExceptions);
}
finally
{
@ -190,10 +203,10 @@ namespace Avalonia.Rendering.Composition.Server
}
}
else
RenderReentrancySafe(catchExceptions);
return RenderReentrancySafe(catchExceptions);
}
private void RenderReentrancySafe(bool catchExceptions)
private bool RenderReentrancySafe(bool catchExceptions)
{
lock (_lock)
{
@ -202,7 +215,7 @@ namespace Avalonia.Rendering.Composition.Server
try
{
_safeThread = Thread.CurrentThread;
RenderCore(catchExceptions);
return RenderCore(catchExceptions);
}
finally
{
@ -235,17 +248,16 @@ namespace Avalonia.Rendering.Composition.Server
return Stopwatch.GetElapsedTime(compositorGlobalPassesStarted);
}
private void RenderCore(bool catchExceptions)
private bool RenderCore(bool catchExceptions)
{
UpdateServerTime();
var compositorGlobalPassesElapsed = ExecuteGlobalPasses();
try
{
if(!RenderInterface.IsReady)
return;
if (!RenderInterface.IsReady)
return true;
RenderInterface.EnsureValidBackendContext();
ExecuteServerJobs(_receivedJobQueue);
@ -263,6 +275,18 @@ namespace Avalonia.Rendering.Composition.Server
{
Logger.TryGet(LogEventLevel.Error, LogArea.Visual)?.Log(this, "Exception when rendering: {Error}", e);
}
// Request a tick if we have active animations or if there are recent batches
if (Animations.NeedNextTick || _ticksSinceLastCommit < CommitGraceTicks)
return true;
// Request a tick if we had unready targets in the last tick, to check if they are ready next time
foreach (var target in _activeTargets)
if (target.IsWaitingForReadyRenderTarget)
return true;
// Otherwise there is no need to waste CPU cycles, tell the timer to pause
return false;
}
public void AddCompositionTarget(ServerCompositionTarget target)

2
src/Avalonia.Base/Rendering/Composition/Server/ServerCompositorAnimations.cs

@ -30,6 +30,8 @@ internal class ServerCompositorAnimations
_dirtyAnimatedObjects.Clear();
}
public bool NeedNextTick => _clockItems.Count > 0;
public void AddDirtyAnimatedObject(ServerObjectAnimations obj)
{
if (_dirtyAnimatedObjects.Add(obj))

5
src/Avalonia.Base/Rendering/Composition/Transport/BatchStream.cs

@ -33,11 +33,8 @@ static unsafe class UnalignedMemoryHelper
{
public static T ReadUnaligned<T>(byte* src) where T : unmanaged
{
#if NET6_0_OR_GREATER
Unsafe.SkipInit<T>(out var rv);
#else
T rv;
#endif
UnalignedMemcpy((byte*)&rv, src, Unsafe.SizeOf<T>());
return rv;
}

75
src/Avalonia.Base/Rendering/Composition/Transport/BatchStreamArrayPool.cs

@ -1,5 +1,6 @@
using System;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using System.Runtime.InteropServices;
using System.Threading;
@ -13,52 +14,75 @@ namespace Avalonia.Rendering.Composition.Transport;
/// </summary>
internal abstract class BatchStreamPoolBase<T> : IDisposable
{
private readonly Action<Func<bool>>? _startTimer;
readonly Stack<T> _pool = new();
bool _disposed;
int _usage;
readonly int[] _usageStatistics = new int[10];
int _usageStatisticsSlot;
readonly bool _reclaimImmediately;
private readonly WeakReference<BatchStreamPoolBase<T>> _updateRef;
private readonly Dispatcher? _reclaimOnDispatcher;
private bool _timerIsRunning;
private ulong _currentUpdateTick, _lastActivityTick;
public int CurrentUsage => _usage;
public int CurrentPool => _pool.Count;
public BatchStreamPoolBase(bool needsFinalize, bool reclaimImmediately, Action<Func<bool>>? startTimer = null)
{
_startTimer = startTimer;
if(!needsFinalize)
GC.SuppressFinalize(needsFinalize);
GC.SuppressFinalize(this);
var updateRef = new WeakReference<BatchStreamPoolBase<T>>(this);
if (
reclaimImmediately
|| Dispatcher.FromThread(Thread.CurrentThread) == null)
_reclaimImmediately = true;
else
StartUpdateTimer(startTimer, updateRef);
_updateRef = new WeakReference<BatchStreamPoolBase<T>>(this);
_reclaimOnDispatcher = !reclaimImmediately ? Dispatcher.FromThread(Thread.CurrentThread) : null;
EnsureUpdateTimer();
}
static void StartUpdateTimer(Action<Func<bool>>? startTimer, WeakReference<BatchStreamPoolBase<T>> updateRef)
void EnsureUpdateTimer()
{
Func<bool> timerProc = () =>
if (_timerIsRunning || !NeedsTimer)
return;
var timerProc = GetTimerProc(_updateRef);
if (_startTimer != null)
_startTimer(timerProc);
else
{
if (updateRef.TryGetTarget(out var target))
if (_reclaimOnDispatcher != null)
{
target.UpdateStatistics();
return true;
if (_reclaimOnDispatcher.CheckAccess())
DispatcherTimer.Run(timerProc, TimeSpan.FromSeconds(1));
else
_reclaimOnDispatcher.Post(
() => DispatcherTimer.Run(timerProc, TimeSpan.FromSeconds(1)),
DispatcherPriority.Normal);
}
}
_timerIsRunning = true;
// Explicit capture
static Func<bool> GetTimerProc(WeakReference<BatchStreamPoolBase<T>> updateRef) => () =>
{
if (updateRef.TryGetTarget(out var target))
return target.UpdateTimerTick();
return false;
};
if (startTimer != null)
startTimer(timerProc);
else
DispatcherTimer.Run(timerProc, TimeSpan.FromSeconds(1));
}
private void UpdateStatistics()
[MemberNotNullWhen(true, nameof(_reclaimOnDispatcher))]
private bool NeedsTimer => _reclaimOnDispatcher != null &&
_currentUpdateTick - _lastActivityTick < (uint)_usageStatistics.Length * 2 + 1;
private bool ReclaimImmediately => _reclaimOnDispatcher == null;
private bool UpdateTimerTick()
{
lock (_pool)
{
_currentUpdateTick++;
var maximumUsage = _usageStatistics.Max();
var recentlyUsedPooledSlots = maximumUsage - _usage;
var keepSlots = Math.Max(recentlyUsedPooledSlots, 10);
@ -67,9 +91,17 @@ internal abstract class BatchStreamPoolBase<T> : IDisposable
_usageStatisticsSlot = (_usageStatisticsSlot + 1) % _usageStatistics.Length;
_usageStatistics[_usageStatisticsSlot] = 0;
return _timerIsRunning = NeedsTimer;
}
}
private void OnActivity()
{
_lastActivityTick = _currentUpdateTick;
EnsureUpdateTimer();
}
protected abstract T CreateItem();
protected virtual void ClearItem(T item)
@ -90,6 +122,8 @@ internal abstract class BatchStreamPoolBase<T> : IDisposable
if (_usageStatistics[_usageStatisticsSlot] < _usage)
_usageStatistics[_usageStatisticsSlot] = _usage;
OnActivity();
if (_pool.Count != 0)
return _pool.Pop();
}
@ -103,9 +137,10 @@ internal abstract class BatchStreamPoolBase<T> : IDisposable
lock (_pool)
{
_usage--;
if (!_disposed && !_reclaimImmediately)
if (!_disposed && !ReclaimImmediately)
{
_pool.Push(item);
OnActivity();
return;
}
}

44
src/Avalonia.Base/Rendering/DefaultRenderTimer.cs

@ -15,8 +15,7 @@ namespace Avalonia.Rendering
[PrivateApi]
public class DefaultRenderTimer : IRenderTimer
{
private int _subscriberCount;
private Action<TimeSpan>? _tick;
private volatile Action<TimeSpan>? _tick;
private IDisposable? _subscription;
/// <summary>
@ -36,40 +35,28 @@ namespace Avalonia.Rendering
public int FramesPerSecond { get; }
/// <inheritdoc/>
public event Action<TimeSpan> Tick
public Action<TimeSpan>? Tick
{
add
get => _tick;
set
{
_tick += value;
if (_subscriberCount++ == 0)
if (value != null)
{
Start();
_tick = value;
_subscription ??= StartCore(InternalTick);
}
}
remove
{
if (--_subscriberCount == 0)
else
{
Stop();
_subscription?.Dispose();
_subscription = null;
_tick = null;
}
_tick -= value;
}
}
/// <inheritdoc />
public virtual bool RunsInBackground => true;
/// <summary>
/// Starts the timer.
/// </summary>
protected void Start()
{
_subscription = StartCore(InternalTick);
}
/// <summary>
/// Provides the implementation of starting the timer.
/// </summary>
@ -85,15 +72,6 @@ namespace Avalonia.Rendering
return new Timer(_ => tick(TimeSpan.FromMilliseconds(Environment.TickCount)), null, interval, interval);
}
/// <summary>
/// Stops the timer.
/// </summary>
protected void Stop()
{
_subscription?.Dispose();
_subscription = null;
}
private void InternalTick(TimeSpan tickCount)
{
_tick?.Invoke(tickCount);

16
src/Avalonia.Base/Rendering/IRenderLoop.cs

@ -9,8 +9,8 @@ namespace Avalonia.Rendering
/// The render loop is responsible for advancing the animation timer and updating the scene
/// graph for visible windows.
/// </remarks>
[NotClientImplementable]
internal interface IRenderLoop
[PrivateApi]
public interface IRenderLoop
{
/// <summary>
/// Adds an update task.
@ -20,17 +20,23 @@ namespace Avalonia.Rendering
/// Registered update tasks will be polled on each tick of the render loop after the
/// animation timer has been pulsed.
/// </remarks>
void Add(IRenderLoopTask i);
internal void Add(IRenderLoopTask i);
/// <summary>
/// Removes an update task.
/// </summary>
/// <param name="i">The update task.</param>
void Remove(IRenderLoopTask i);
internal void Remove(IRenderLoopTask i);
/// <summary>
/// Indicates if the rendering is done on a non-UI thread.
/// </summary>
bool RunsInBackground { get; }
internal bool RunsInBackground { get; }
/// <summary>
/// Wakes up the render loop to schedule the next tick.
/// Thread-safe: can be called from any thread.
/// </summary>
internal void Wakeup();
}
}

5
src/Avalonia.Base/Rendering/IRenderLoopTask.cs

@ -1,10 +1,7 @@
using System;
using System.Threading.Tasks;
namespace Avalonia.Rendering
{
internal interface IRenderLoopTask
{
void Render();
bool Render();
}
}

13
src/Avalonia.Base/Rendering/IRenderTimer.cs

@ -10,16 +10,19 @@ namespace Avalonia.Rendering
public interface IRenderTimer
{
/// <summary>
/// Raised when the render timer ticks to signal a new frame should be drawn.
/// Gets or sets the callback to be invoked when the timer ticks.
/// This property can be set from any thread, but it's guaranteed that it's not set concurrently
/// (i. e. render loop always does it under a lock).
/// Setting the value to null suggests the timer to stop ticking, however
/// timer is allowed to produce ticks on the previously set value as long as it stops doing so
/// </summary>
/// <remarks>
/// This event can be raised on any thread; it is the responsibility of the subscriber to
/// switch execution to the right thread.
/// The callback can be invoked on any thread
/// </remarks>
event Action<TimeSpan> Tick;
Action<TimeSpan>? Tick { get; set; }
/// <summary>
/// Indicates if the timer ticks on a non-UI thread
/// Indicates if the timer ticks on a non-UI thread.
/// </summary>
bool RunsInBackground { get; }
}

140
src/Avalonia.Base/Rendering/RenderLoop.cs

@ -2,58 +2,52 @@
using System.Collections.Generic;
using System.Threading;
using Avalonia.Logging;
using Avalonia.Metadata;
using Avalonia.Threading;
namespace Avalonia.Rendering
{
/// <summary>
/// The application render loop.
/// Provides factory methods for creating <see cref="IRenderLoop"/> instances.
/// </summary>
[PrivateApi]
public static class RenderLoop
{
/// <summary>
/// Creates an <see cref="IRenderLoop"/> from an <see cref="IRenderTimer"/>.
/// </summary>
public static IRenderLoop FromTimer(IRenderTimer timer) => new DefaultRenderLoop(timer);
}
/// <summary>
/// Default implementation of the application render loop.
/// </summary>
/// <remarks>
/// The render loop is responsible for advancing the animation timer and updating the scene
/// graph for visible windows.
/// graph for visible windows. It owns the sleep/wake state machine: setting
/// <see cref="IRenderTimer.Tick"/> to a non-null callback to start the timer and to null to
/// stop it, under a lock so that timer implementations never see concurrent changes.
/// </remarks>
internal class RenderLoop : IRenderLoop
internal class DefaultRenderLoop : IRenderLoop
{
private readonly List<IRenderLoopTask> _items = new List<IRenderLoopTask>();
private readonly List<IRenderLoopTask> _itemsCopy = new List<IRenderLoopTask>();
private IRenderTimer? _timer;
private Action<TimeSpan> _tick;
private readonly IRenderTimer _timer;
private readonly object _timerLock = new();
private int _inTick;
public static IRenderLoop LocatorAutoInstance
{
get
{
var loop = AvaloniaLocator.Current.GetService<IRenderLoop>();
if (loop == null)
{
var timer = AvaloniaLocator.Current.GetRequiredService<IRenderTimer>();
AvaloniaLocator.CurrentMutable.Bind<IRenderLoop>()
.ToConstant(loop = new RenderLoop(timer));
}
return loop;
}
}
private volatile bool _hasItems;
private bool _running;
private bool _wakeupPending;
/// <summary>
/// Initializes a new instance of the <see cref="RenderLoop"/> class.
/// Initializes a new instance of the <see cref="DefaultRenderLoop"/> class.
/// </summary>
/// <param name="timer">The render timer.</param>
public RenderLoop(IRenderTimer timer)
public DefaultRenderLoop(IRenderTimer timer)
{
_timer = timer;
}
/// <summary>
/// Gets the render timer.
/// </summary>
protected IRenderTimer Timer
{
get
{
return _timer ??= AvaloniaLocator.Current.GetRequiredService<IRenderTimer>();
}
_tick = TimerTick;
}
/// <inheritdoc/>
@ -62,14 +56,17 @@ namespace Avalonia.Rendering
_ = i ?? throw new ArgumentNullException(nameof(i));
Dispatcher.UIThread.VerifyAccess();
bool shouldStart;
lock (_items)
{
_items.Add(i);
shouldStart = _items.Count == 1;
}
if (_items.Count == 1)
{
Timer.Tick += TimerTick;
}
if (shouldStart)
{
_hasItems = true;
Wakeup();
}
}
@ -78,19 +75,48 @@ namespace Avalonia.Rendering
{
_ = i ?? throw new ArgumentNullException(nameof(i));
Dispatcher.UIThread.VerifyAccess();
bool shouldStop;
lock (_items)
{
_items.Remove(i);
shouldStop = _items.Count == 0;
}
if (_items.Count == 0)
if (shouldStop)
{
_hasItems = false;
lock (_timerLock)
{
Timer.Tick -= TimerTick;
if (_running)
{
_running = false;
_wakeupPending = false;
_timer.Tick = null;
}
}
}
}
/// <inheritdoc />
public bool RunsInBackground => Timer.RunsInBackground;
public bool RunsInBackground => _timer.RunsInBackground;
/// <inheritdoc />
public void Wakeup()
{
lock (_timerLock)
{
if (_hasItems && !_running)
{
_running = true;
_timer.Tick = _tick;
}
else
{
_wakeupPending = true;
}
}
}
private void TimerTick(TimeSpan time)
{
@ -98,21 +124,49 @@ namespace Avalonia.Rendering
{
try
{
// Consume any pending wakeup — this tick will process its work.
// Only wakeups arriving during task execution will keep the timer running.
// Also drop late ticks that arrive after the timer was stopped.
lock (_timerLock)
{
if (!_running)
return;
_wakeupPending = false;
}
lock (_items)
{
_itemsCopy.Clear();
_itemsCopy.AddRange(_items);
}
var wantsNextTick = false;
for (int i = 0; i < _itemsCopy.Count; i++)
{
_itemsCopy[i].Render();
wantsNextTick |= _itemsCopy[i].Render();
}
_itemsCopy.Clear();
if (!wantsNextTick)
{
lock (_timerLock)
{
if (!_running)
{
// Already stopped by Remove()
}
else if (_wakeupPending)
{
_wakeupPending = false;
}
else
{
_running = false;
_timer.Tick = null;
}
}
}
}
catch (Exception ex)
{

61
src/Avalonia.Base/Rendering/SleepLoopRenderTimer.cs

@ -8,10 +8,10 @@ namespace Avalonia.Rendering
[PrivateApi]
public class SleepLoopRenderTimer : IRenderTimer
{
private Action<TimeSpan>? _tick;
private int _count;
private readonly object _lock = new object();
private bool _running;
private volatile Action<TimeSpan>? _tick;
private volatile bool _stopped = true;
private bool _threadStarted;
private readonly AutoResetEvent _wakeEvent = new(false);
private readonly Stopwatch _st = Stopwatch.StartNew();
private readonly TimeSpan _timeBetweenTicks;
@ -19,28 +19,30 @@ namespace Avalonia.Rendering
{
_timeBetweenTicks = TimeSpan.FromSeconds(1d / fps);
}
public event Action<TimeSpan> Tick
public Action<TimeSpan>? Tick
{
add
get => _tick;
set
{
lock (_lock)
if (value != null)
{
_tick += value;
_count++;
if (_running)
return;
_running = true;
new Thread(LoopProc) { IsBackground = true }.Start();
_tick = value;
_stopped = false;
if (!_threadStarted)
{
_threadStarted = true;
new Thread(LoopProc) { IsBackground = true }.Start();
}
else
{
_wakeEvent.Set();
}
}
}
remove
{
lock (_lock)
else
{
_tick -= value;
_count--;
_stopped = true;
_tick = null;
}
}
}
@ -52,24 +54,17 @@ namespace Avalonia.Rendering
var lastTick = _st.Elapsed;
while (true)
{
if (_stopped)
_wakeEvent.WaitOne();
var now = _st.Elapsed;
var timeTillNextTick = lastTick + _timeBetweenTicks - now;
if (timeTillNextTick.TotalMilliseconds > 1) Thread.Sleep(timeTillNextTick);
if (timeTillNextTick.TotalMilliseconds > 1)
_wakeEvent.WaitOne(timeTillNextTick);
lastTick = now = _st.Elapsed;
lock (_lock)
{
if (_count == 0)
{
_running = false;
return;
}
}
_tick?.Invoke(now);
}
}
}
}

65
src/Avalonia.Base/Rendering/ThreadProxyRenderTimer.cs

@ -12,8 +12,9 @@ public sealed class ThreadProxyRenderTimer : IRenderTimer
private readonly Stopwatch _stopwatch;
private readonly Thread _timerThread;
private readonly AutoResetEvent _autoResetEvent;
private Action<TimeSpan>? _tick;
private int _subscriberCount;
private readonly object _lock = new();
private volatile Action<TimeSpan>? _tick;
private volatile bool _active;
private bool _registered;
public ThreadProxyRenderTimer(IRenderTimer inner, int maxStackSize = 1 * 1024 * 1024)
@ -24,33 +25,54 @@ public sealed class ThreadProxyRenderTimer : IRenderTimer
_timerThread = new Thread(RenderTimerThreadFunc, maxStackSize) { Name = "RenderTimerLoop", IsBackground = true };
}
public event Action<TimeSpan> Tick
public Action<TimeSpan>? Tick
{
add
get => _tick;
set
{
_tick += value;
if (!_registered)
lock (_lock)
{
_registered = true;
_timerThread.Start();
if (value != null)
{
_tick = value;
_active = true;
EnsureStarted();
_inner.Tick = InnerTick;
}
else
{
// Don't set _inner.Tick = null here — may be on the wrong thread.
// InnerTick will detect _active=false and clear _inner.Tick on the correct thread.
_active = false;
_tick = null;
}
}
}
}
if (_subscriberCount++ == 0)
{
_inner.Tick += InnerTick;
}
public bool RunsInBackground => true;
private void EnsureStarted()
{
if (!_registered)
{
_registered = true;
_stopwatch.Start();
_timerThread.Start();
}
}
remove
private void InnerTick(TimeSpan obj)
{
lock (_lock)
{
if (--_subscriberCount == 0)
if (!_active)
{
_inner.Tick -= InnerTick;
_inner.Tick = null;
return;
}
_tick -= value;
}
_autoResetEvent.Set();
}
private void RenderTimerThreadFunc()
@ -60,11 +82,4 @@ public sealed class ThreadProxyRenderTimer : IRenderTimer
_tick?.Invoke(_stopwatch.Elapsed);
}
}
private void InnerTick(TimeSpan obj)
{
_autoResetEvent.Set();
}
public bool RunsInBackground => true;
}

3
src/Avalonia.Base/Threading/AvaloniaSynchronizationContext.cs

@ -74,9 +74,6 @@ namespace Avalonia.Threading
_dispatcher.Send(d, state, Priority);
}
#if !NET6_0_OR_GREATER
[PrePrepareMethod]
#endif
public override int Wait(IntPtr[] waitHandles, bool waitAll, int millisecondsTimeout)
{
if (

156
src/Avalonia.Base/Threading/CulturePreservingExecutionContext.cs

@ -1,156 +0,0 @@
#if NET6_0_OR_GREATER
// In .NET Core, the security context and call context are not supported, however,
// the impersonation context and culture would typically flow with the execution context.
// See: https://learn.microsoft.com/en-us/dotnet/api/system.threading.executioncontext
//
// So we can safely use ExecutionContext without worrying about culture flowing issues.
#else
using System;
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using System.Globalization;
using System.Threading;
namespace Avalonia.Threading;
/// <summary>
/// An ExecutionContext that preserves culture information across async operations.
/// This is a modernized version that removes legacy compatibility switches and
/// includes nullable reference type annotations.
/// </summary>
internal sealed class CulturePreservingExecutionContext
{
private readonly ExecutionContext _context;
private CultureAndContext? _cultureAndContext;
private CulturePreservingExecutionContext(ExecutionContext context)
{
_context = context;
}
/// <summary>
/// Captures the current ExecutionContext and culture information.
/// </summary>
/// <returns>A new CulturePreservingExecutionContext instance, or null if no context needs to be captured.</returns>
public static CulturePreservingExecutionContext? Capture()
{
// ExecutionContext.SuppressFlow had been called.
// We expect ExecutionContext.Capture() to return null, so match that behavior and return null.
if (ExecutionContext.IsFlowSuppressed())
{
return null;
}
var context = ExecutionContext.Capture();
if (context == null)
return null;
return new CulturePreservingExecutionContext(context);
}
/// <summary>
/// Runs the specified callback in the captured execution context while preserving culture information.
/// This method is used for .NET Framework and earlier .NET versions.
/// </summary>
/// <param name="executionContext">The execution context to run in.</param>
/// <param name="callback">The callback to execute.</param>
/// <param name="state">The state to pass to the callback.</param>
public static void Run(CulturePreservingExecutionContext executionContext, ContextCallback callback, object? state)
{
// ReSharper disable once ConditionIsAlwaysTrueOrFalseAccordingToNullableAPIContract
if (callback == null)
return;
// ReSharper disable once ConditionIsAlwaysTrueOrFalseAccordingToNullableAPIContract
if (executionContext == null)
ThrowNullContext();
// Save culture information - we will need this to restore just before
// the callback is actually invoked from CallbackWrapper.
executionContext._cultureAndContext = CultureAndContext.Initialize(callback, state);
try
{
ExecutionContext.Run(
executionContext._context,
s_callbackWrapperDelegate,
executionContext._cultureAndContext);
}
finally
{
// Restore culture information - it might have been modified during callback execution.
executionContext._cultureAndContext.RestoreCultureInfos();
}
}
[DoesNotReturn]
private static void ThrowNullContext()
{
throw new InvalidOperationException("ExecutionContext cannot be null.");
}
private static readonly ContextCallback s_callbackWrapperDelegate = CallbackWrapper;
/// <summary>
/// Executes the callback and saves culture values immediately afterwards.
/// </summary>
/// <param name="obj">Contains the actual callback and state.</param>
private static void CallbackWrapper(object? obj)
{
var cultureAndContext = (CultureAndContext)obj!;
// Restore culture information saved during Run()
cultureAndContext.RestoreCultureInfos();
try
{
// Execute the actual callback
cultureAndContext.Callback(cultureAndContext.State);
}
finally
{
// Save any culture changes that might have occurred during callback execution
cultureAndContext.CaptureCultureInfos();
}
}
/// <summary>
/// Helper class to manage culture information across execution contexts.
/// </summary>
private sealed class CultureAndContext
{
public ContextCallback Callback { get; }
public object? State { get; }
private CultureInfo? _culture;
private CultureInfo? _uiCulture;
private CultureAndContext(ContextCallback callback, object? state)
{
Callback = callback;
State = state;
CaptureCultureInfos();
}
public static CultureAndContext Initialize(ContextCallback callback, object? state)
{
return new CultureAndContext(callback, state);
}
public void CaptureCultureInfos()
{
_culture = Thread.CurrentThread.CurrentCulture;
_uiCulture = Thread.CurrentThread.CurrentUICulture;
}
public void RestoreCultureInfos()
{
if (_culture != null)
Thread.CurrentThread.CurrentCulture = _culture;
if (_uiCulture != null)
Thread.CurrentThread.CurrentUICulture = _uiCulture;
}
}
}
#endif

8
src/Avalonia.Base/Threading/DispatcherOperation.cs

@ -5,11 +5,7 @@ using System.Runtime.CompilerServices;
using System.Threading;
using System.Threading.Tasks;
#if NET6_0_OR_GREATER
using ExecutionContext = System.Threading.ExecutionContext;
#else
using ExecutionContext = Avalonia.Threading.CulturePreservingExecutionContext;
#endif
namespace Avalonia.Threading;
@ -277,12 +273,8 @@ public class DispatcherOperation
{
if (_executionContext is { } executionContext)
{
#if NET6_0_OR_GREATER
ExecutionContext.Restore(executionContext);
InvokeCore();
#else
ExecutionContext.Run(executionContext, static s => ((DispatcherOperation)s!).InvokeCore(), this);
#endif
}
else
{

7
src/Avalonia.Base/Threading/NonPumpingSyncContext.cs

@ -22,11 +22,7 @@ namespace Avalonia.Threading
{
if (_inner is null)
{
#if NET6_0_OR_GREATER
ThreadPool.QueueUserWorkItem(static x => x.d(x.state), (d, state), false);
#else
ThreadPool.QueueUserWorkItem(_ => d(state));
#endif
}
else
{
@ -46,9 +42,6 @@ namespace Avalonia.Threading
}
}
#if !NET6_0_OR_GREATER
[PrePrepareMethod]
#endif
public override int Wait(IntPtr[] waitHandles, bool waitAll, int millisecondsTimeout) =>
_impl.Wait(waitHandles, waitAll, millisecondsTimeout);

4
src/Avalonia.Base/Utilities/ArrayBuilder.cs

@ -136,7 +136,6 @@ namespace Avalonia.Utilities
/// </summary>
public void Clear()
{
#if NET6_0_OR_GREATER
if (RuntimeHelpers.IsReferenceOrContainsReferences<T>())
{
ClearArray();
@ -145,9 +144,6 @@ namespace Avalonia.Utilities
{
_size = 0;
}
#else
ClearArray();
#endif
}
private void ClearArray()

10
src/Avalonia.Base/Utilities/AvaloniaPropertyDictionary.cs

@ -244,12 +244,8 @@ namespace Avalonia.Utilities
// hi and lo are never negative: there's no overflow using unsigned math
var i = (int)(((uint)hi + (uint)lo) >> 1);
#if NET6_0_OR_GREATER
// nuint cast to force zero extend instead of sign extend
ref var entry = ref Unsafe.Add(ref entry0, (nuint)i);
#else
ref var entry = ref Unsafe.Add(ref entry0, i);
#endif
var entryId = entry.Id;
if (entryId == propertyId)
@ -288,12 +284,8 @@ namespace Avalonia.Utilities
// hi and lo are never negative: there's no overflow using unsigned math
var i = (int)(((uint)hi + (uint)lo) >> 1);
#if NET6_0_OR_GREATER
// nuint cast to force zero extend instead of sign extend
ref var entry = ref Unsafe.Add(ref entry0, (nuint)i);
#else
ref var entry = ref Unsafe.Add(ref entry0, i);
#endif
var entryId = entry.Id;
if (entryId == propertyId)
@ -360,7 +352,7 @@ namespace Avalonia.Utilities
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private ref Entry UnsafeGetEntryRef(int index)
{
#if NET6_0_OR_GREATER && !DEBUG
#if !DEBUG
// This type is performance critical: in release mode, skip any bound check the JIT compiler couldn't elide.
// The index parameter should always be correct when calling this method: no unchecked user input should get here.
return ref Unsafe.Add(ref MemoryMarshal.GetArrayDataReference(_entries!), (uint)index);

34
src/Avalonia.Base/Utilities/EnumHelper.cs

@ -1,34 +0,0 @@
using System;
namespace Avalonia.Utilities
{
internal class EnumHelper
{
#if NET6_0_OR_GREATER
public static T Parse<T>(ReadOnlySpan<char> key, bool ignoreCase) where T : struct
{
return Enum.Parse<T>(key, ignoreCase);
}
public static bool TryParse<T>(ReadOnlySpan<char> key, bool ignoreCase, out T result) where T : struct
{
return Enum.TryParse(key, ignoreCase, out result);
}
#else
public static T Parse<T>(string key, bool ignoreCase) where T : struct
{
return (T)Enum.Parse(typeof(T), key, ignoreCase);
}
public static bool TryParse<T>(string key, bool ignoreCase, out T result) where T : struct
{
return Enum.TryParse(key, ignoreCase, out result);
}
public static bool TryParse<T>(ReadOnlySpan<char> key, bool ignoreCase, out T result) where T : struct
{
return Enum.TryParse(key.ToString(), ignoreCase, out result);
}
#endif
}
}

Some files were not shown because too many files changed in this diff

Loading…
Cancel
Save