diff --git a/.github/workflows/api-diff.yml b/.github/workflows/api-diff.yml new file mode 100644 index 0000000000..e87f3f334f --- /dev/null +++ b/.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
if large + if (body.length > 2000) { + const inner = body; + body = '
\n📋 API Diff (click to expand)\n\n' + inner + '\n\n
'; + } + + // Update existing bot comment or create a new one + const marker = ''; + 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}).`, + }); diff --git a/.github/workflows/update-api.yml b/.github/workflows/update-api.yml new file mode 100644 index 0000000000..27a0598d3b --- /dev/null +++ b/.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}).`, + }); diff --git a/.gitignore b/.gitignore index 826b4d8a5a..23b8efffa0 100644 --- a/.gitignore +++ b/.gitignore @@ -219,3 +219,4 @@ src/Browser/Avalonia.Browser.Blazor/wwwroot src/Browser/Avalonia.Browser/wwwroot api/diff src/Browser/Avalonia.Browser/staticwebassets +.serena diff --git a/.gitmodules b/.gitmodules index 07f532607a..2d648aa7ba 100644 --- a/.gitmodules +++ b/.gitmodules @@ -1,6 +1,6 @@ -[submodule "Numerge"] - path = external/Numerge - url = https://github.com/kekekeks/Numerge.git [submodule "XamlX"] path = external/XamlX url = https://github.com/kekekeks/XamlX.git +[submodule "Avalonia.DBus"] + path = external/Avalonia.DBus + url = https://github.com/AvaloniaUI/Avalonia.DBus.git diff --git a/Avalonia.Desktop.slnf b/Avalonia.Desktop.slnf index b861cf00c5..dfa0945890 100644 --- a/Avalonia.Desktop.slnf +++ b/Avalonia.Desktop.slnf @@ -8,11 +8,11 @@ "samples\\ControlCatalog\\ControlCatalog.csproj", "samples\\GpuInterop\\GpuInterop.csproj", "samples\\IntegrationTestApp\\IntegrationTestApp.csproj", - "samples\\TextTestApp\\TextTestApp.csproj", "samples\\MiniMvvm\\MiniMvvm.csproj", "samples\\RenderDemo\\RenderDemo.csproj", "samples\\SampleControls\\ControlSamples.csproj", "samples\\Sandbox\\Sandbox.csproj", + "samples\\TextTestApp\\TextTestApp.csproj", "samples\\UnloadableAssemblyLoadContext\\UnloadableAssemblyLoadContextPlug\\UnloadableAssemblyLoadContextPlug.csproj", "samples\\UnloadableAssemblyLoadContext\\UnloadableAssemblyLoadContext\\UnloadableAssemblyLoadContext.csproj", "samples\\XEmbedSample\\XEmbedSample.csproj", @@ -24,18 +24,20 @@ "src\\Avalonia.Desktop\\Avalonia.Desktop.csproj", "src\\Avalonia.Dialogs\\Avalonia.Dialogs.csproj", "src\\Avalonia.Fonts.Inter\\Avalonia.Fonts.Inter.csproj", + "src\\Avalonia.FreeDesktop.AtSpi\\Avalonia.FreeDesktop.AtSpi.csproj", "src\\Avalonia.FreeDesktop\\Avalonia.FreeDesktop.csproj", "src\\Avalonia.Metal\\Avalonia.Metal.csproj", "src\\Avalonia.MicroCom\\Avalonia.MicroCom.csproj", "src\\Avalonia.Native\\Avalonia.Native.csproj", "src\\Avalonia.OpenGL\\Avalonia.OpenGL.csproj", - "src\\Avalonia.Vulkan\\Avalonia.Vulkan.csproj", "src\\Avalonia.Remote.Protocol\\Avalonia.Remote.Protocol.csproj", "src\\Avalonia.Themes.Fluent\\Avalonia.Themes.Fluent.csproj", "src\\Avalonia.Themes.Simple\\Avalonia.Themes.Simple.csproj", + "src\\Avalonia.Vulkan\\Avalonia.Vulkan.csproj", "src\\Avalonia.X11\\Avalonia.X11.csproj", "src\\HarfBuzz\\Avalonia.HarfBuzz\\Avalonia.HarfBuzz.csproj", "src\\Headless\\Avalonia.Headless.Vnc\\Avalonia.Headless.Vnc.csproj", + "src\\Headless\\Avalonia.Headless.XUnit\\Avalonia.Headless.XUnit.csproj", "src\\Headless\\Avalonia.Headless\\Avalonia.Headless.csproj", "src\\Linux\\Avalonia.LinuxFramebuffer\\Avalonia.LinuxFramebuffer.csproj", "src\\Markup\\Avalonia.Markup.Xaml.Loader\\Avalonia.Markup.Xaml.Loader.csproj", @@ -45,6 +47,7 @@ "src\\tools\\Avalonia.Analyzers.CodeFixes.CSharp\\Avalonia.Analyzers.CodeFixes.CSharp.csproj", "src\\tools\\Avalonia.Analyzers.CSharp\\Avalonia.Analyzers.CSharp.csproj", "src\\tools\\Avalonia.Analyzers.VisualBasic\\Avalonia.Analyzers.VisualBasic.csproj", + "src\\tools\\Avalonia.DBus.Generators\\Avalonia.DBus.Generators.csproj", "src\\tools\\Avalonia.Generators\\Avalonia.Generators.csproj", "src\\tools\\DevAnalyzers\\DevAnalyzers.csproj", "src\\tools\\DevGenerators\\DevGenerators.csproj", @@ -58,7 +61,10 @@ "tests\\Avalonia.DesignerSupport.TestApp\\Avalonia.DesignerSupport.TestApp.csproj", "tests\\Avalonia.DesignerSupport.Tests\\Avalonia.DesignerSupport.Tests.csproj", "tests\\Avalonia.Generators.Tests\\Avalonia.Generators.Tests.csproj", + "tests\\Avalonia.Headless.XUnit.PerAssembly.UnitTests\\Avalonia.Headless.XUnit.PerAssembly.UnitTests.csproj", + "tests\\Avalonia.Headless.XUnit.PerTest.UnitTests\\Avalonia.Headless.XUnit.PerTest.UnitTests.csproj", "tests\\Avalonia.IntegrationTests.Appium\\Avalonia.IntegrationTests.Appium.csproj", + "tests\\Avalonia.IntegrationTests.Win32\\Avalonia.IntegrationTests.Win32.csproj", "tests\\Avalonia.LeakTests\\Avalonia.LeakTests.csproj", "tests\\Avalonia.Markup.UnitTests\\Avalonia.Markup.UnitTests.csproj", "tests\\Avalonia.Markup.Xaml.UnitTests\\Avalonia.Markup.Xaml.UnitTests.csproj", @@ -69,4 +75,4 @@ "tests\\TestFiles\\BuildTasks\\PInvoke\\PInvoke.csproj" ] } -} +} \ No newline at end of file diff --git a/Avalonia.sln b/Avalonia.sln index 06c3e051ac..207f673c26 100644 --- a/Avalonia.sln +++ b/Avalonia.sln @@ -83,21 +83,13 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Props", "Props", "{F3AC8BC1 build\AnalyzerProject.targets = build\AnalyzerProject.targets build\AvaloniaPublicKey.props = build\AvaloniaPublicKey.props build\Base.props = build\Base.props - build\Binding.props = build\Binding.props build\CoreLibraries.props = build\CoreLibraries.props build\DevAnalyzers.props = build\DevAnalyzers.props build\EmbedXaml.props = build\EmbedXaml.props build\HarfBuzzSharp.props = build\HarfBuzzSharp.props - build\ImageSharp.props = build\ImageSharp.props - build\Microsoft.CSharp.props = build\Microsoft.CSharp.props - build\Microsoft.Reactive.Testing.props = build\Microsoft.Reactive.Testing.props - build\Moq.props = build\Moq.props build\NetAnalyzers.props = build\NetAnalyzers.props - build\NetCore.props = build\NetCore.props - build\NetFX.props = build\NetFX.props build\NullableEnable.props = build\NullableEnable.props build\ReferenceCoreLibraries.props = build\ReferenceCoreLibraries.props - build\Rx.props = build\Rx.props build\SampleApp.props = build\SampleApp.props build\SharedVersion.props = build\SharedVersion.props build\SkiaSharp.props = build\SkiaSharp.props @@ -113,7 +105,6 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Targets", "Targets", "{4D6F ProjectSection(SolutionItems) = preProject build\BuildTargets.targets = build\BuildTargets.targets build\DevSingleProject.targets = build\DevSingleProject.targets - build\LegacyProject.targets = build\LegacyProject.targets build\UnitTests.NetCore.targets = build\UnitTests.NetCore.targets EndProjectSection EndProject @@ -127,14 +118,10 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Avalonia.Skia.RenderTests", EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Avalonia.Remote.Protocol", "src\Avalonia.Remote.Protocol\Avalonia.Remote.Protocol.csproj", "{D78A720C-C0C6-478B-8564-F167F9BDD01B}" EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "RemoteDemo", "samples\RemoteDemo\RemoteDemo.csproj", "{E2999E4A-9086-401F-898C-AEB0AD38E676}" -EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Tools", "Tools", "{4ED8B739-6F4E-4CD4-B993-545E6B5CE637}" EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Avalonia.Designer.HostApp", "src\tools\Avalonia.Designer.HostApp\Avalonia.Designer.HostApp.csproj", "{050CC912-FF49-4A8B-B534-9544017446DD}" EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Previewer", "samples\Previewer\Previewer.csproj", "{F40FC0A2-1BC3-401C-BFC1-928EC4D4A9CE}" -EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Avalonia.Skia.UnitTests", "tests\Avalonia.Skia.UnitTests\Avalonia.Skia.UnitTests.csproj", "{E1240B49-7B4B-4371-A00E-068778C5CF0B}" EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Avalonia.OpenGL", "src\Avalonia.OpenGL\Avalonia.OpenGL.csproj", "{7CCAEFC4-135D-401D-BDDD-896B9B7D3569}" @@ -220,6 +207,7 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution NOTICE.md = NOTICE.md NuGet.Config = NuGet.Config readme.md = readme.md + Directory.Packages.props = Directory.Packages.props EndProjectSection EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Avalonia.Generators", "src\tools\Avalonia.Generators\Avalonia.Generators.csproj", "{DDA28789-C21A-4654-86CE-D01E81F095C5}" @@ -292,6 +280,10 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Avalonia.Analyzers.CodeFixe EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Avalonia.Analyzers.VisualBasic", "src\tools\Avalonia.Analyzers.VisualBasic\Avalonia.Analyzers.VisualBasic.csproj", "{A7644C3B-B843-44F1-9940-560D56CB0936}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Avalonia.FreeDesktop.AtSpi", "src\Avalonia.FreeDesktop.AtSpi\Avalonia.FreeDesktop.AtSpi.csproj", "{742C3613-514C-4D6B-804A-2A7925F278F3}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Avalonia.DBus.Generators", "src\tools\Avalonia.DBus.Generators\Avalonia.DBus.Generators.csproj", "{98A16FFD-0C99-4665-AC64-DC17E86879A2}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -412,18 +404,10 @@ Global {D78A720C-C0C6-478B-8564-F167F9BDD01B}.Debug|Any CPU.Build.0 = Debug|Any CPU {D78A720C-C0C6-478B-8564-F167F9BDD01B}.Release|Any CPU.ActiveCfg = Release|Any CPU {D78A720C-C0C6-478B-8564-F167F9BDD01B}.Release|Any CPU.Build.0 = Release|Any CPU - {E2999E4A-9086-401F-898C-AEB0AD38E676}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {E2999E4A-9086-401F-898C-AEB0AD38E676}.Debug|Any CPU.Build.0 = Debug|Any CPU - {E2999E4A-9086-401F-898C-AEB0AD38E676}.Release|Any CPU.ActiveCfg = Release|Any CPU - {E2999E4A-9086-401F-898C-AEB0AD38E676}.Release|Any CPU.Build.0 = Release|Any CPU {050CC912-FF49-4A8B-B534-9544017446DD}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {050CC912-FF49-4A8B-B534-9544017446DD}.Debug|Any CPU.Build.0 = Debug|Any CPU {050CC912-FF49-4A8B-B534-9544017446DD}.Release|Any CPU.ActiveCfg = Release|Any CPU {050CC912-FF49-4A8B-B534-9544017446DD}.Release|Any CPU.Build.0 = Release|Any CPU - {F40FC0A2-1BC3-401C-BFC1-928EC4D4A9CE}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {F40FC0A2-1BC3-401C-BFC1-928EC4D4A9CE}.Debug|Any CPU.Build.0 = Debug|Any CPU - {F40FC0A2-1BC3-401C-BFC1-928EC4D4A9CE}.Release|Any CPU.ActiveCfg = Release|Any CPU - {F40FC0A2-1BC3-401C-BFC1-928EC4D4A9CE}.Release|Any CPU.Build.0 = Release|Any CPU {E1240B49-7B4B-4371-A00E-068778C5CF0B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {E1240B49-7B4B-4371-A00E-068778C5CF0B}.Debug|Any CPU.Build.0 = Debug|Any CPU {E1240B49-7B4B-4371-A00E-068778C5CF0B}.Release|Any CPU.ActiveCfg = Release|Any CPU @@ -670,14 +654,22 @@ Global {11522B0D-BF31-42D5-8FC5-41E58F319AF9}.Debug|Any CPU.Build.0 = Debug|Any CPU {11522B0D-BF31-42D5-8FC5-41E58F319AF9}.Release|Any CPU.ActiveCfg = Release|Any CPU {11522B0D-BF31-42D5-8FC5-41E58F319AF9}.Release|Any CPU.Build.0 = Release|Any CPU - {A7644C3B-B843-44F1-9940-560D56CB0936}.Release|Any CPU.ActiveCfg = Release|Any CPU - {A7644C3B-B843-44F1-9940-560D56CB0936}.Release|Any CPU.Build.0 = Release|Any CPU - {A7644C3B-B843-44F1-9940-560D56CB0936}.Debug|Any CPU.ActiveCfg = Release|Any CPU - {A7644C3B-B843-44F1-9940-560D56CB0936}.Debug|Any CPU.Build.0 = Release|Any CPU - {FDFB9C25-552D-420B-9D4A-DB0BB6472239}.Release|Any CPU.ActiveCfg = Release|Any CPU - {FDFB9C25-552D-420B-9D4A-DB0BB6472239}.Release|Any CPU.Build.0 = Release|Any CPU {FDFB9C25-552D-420B-9D4A-DB0BB6472239}.Debug|Any CPU.ActiveCfg = Release|Any CPU {FDFB9C25-552D-420B-9D4A-DB0BB6472239}.Debug|Any CPU.Build.0 = Release|Any CPU + {FDFB9C25-552D-420B-9D4A-DB0BB6472239}.Release|Any CPU.ActiveCfg = Release|Any CPU + {FDFB9C25-552D-420B-9D4A-DB0BB6472239}.Release|Any CPU.Build.0 = Release|Any CPU + {A7644C3B-B843-44F1-9940-560D56CB0936}.Debug|Any CPU.ActiveCfg = Release|Any CPU + {A7644C3B-B843-44F1-9940-560D56CB0936}.Debug|Any CPU.Build.0 = Release|Any CPU + {A7644C3B-B843-44F1-9940-560D56CB0936}.Release|Any CPU.ActiveCfg = Release|Any CPU + {A7644C3B-B843-44F1-9940-560D56CB0936}.Release|Any CPU.Build.0 = Release|Any CPU + {742C3613-514C-4D6B-804A-2A7925F278F3}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {742C3613-514C-4D6B-804A-2A7925F278F3}.Debug|Any CPU.Build.0 = Debug|Any CPU + {742C3613-514C-4D6B-804A-2A7925F278F3}.Release|Any CPU.ActiveCfg = Release|Any CPU + {742C3613-514C-4D6B-804A-2A7925F278F3}.Release|Any CPU.Build.0 = Release|Any CPU + {98A16FFD-0C99-4665-AC64-DC17E86879A2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {98A16FFD-0C99-4665-AC64-DC17E86879A2}.Debug|Any CPU.Build.0 = Debug|Any CPU + {98A16FFD-0C99-4665-AC64-DC17E86879A2}.Release|Any CPU.ActiveCfg = Release|Any CPU + {98A16FFD-0C99-4665-AC64-DC17E86879A2}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -707,9 +699,7 @@ Global {854568D5-13D1-4B4F-B50D-534DC7EFD3C9} = {86C53C40-57AA-45B8-AD42-FAE0EFDF0F2B} {CBC4FF2F-92D4-420B-BE21-9FE0B930B04E} = {B39A8919-9F95-48FE-AD7B-76E08B509888} {E1582370-37B3-403C-917F-8209551B1634} = {C5A00AC3-B34C-4564-9BDD-2DA473EF4D8B} - {E2999E4A-9086-401F-898C-AEB0AD38E676} = {9B9E3891-2366-4253-A952-D08BCEB71098} {050CC912-FF49-4A8B-B534-9544017446DD} = {4ED8B739-6F4E-4CD4-B993-545E6B5CE637} - {F40FC0A2-1BC3-401C-BFC1-928EC4D4A9CE} = {9B9E3891-2366-4253-A952-D08BCEB71098} {E1240B49-7B4B-4371-A00E-068778C5CF0B} = {C5A00AC3-B34C-4564-9BDD-2DA473EF4D8B} {D49233F8-F29C-47DD-9975-C4C9E4502720} = {E870DCD7-F46A-498D-83FC-D0FD13E0A11C} {3C471044-3640-45E3-B1B2-16D2FF8399EE} = {E870DCD7-F46A-498D-83FC-D0FD13E0A11C} @@ -763,8 +753,10 @@ Global {342D2657-2F84-493C-B74B-9D2CAE5D9DAB} = {C5A00AC3-B34C-4564-9BDD-2DA473EF4D8B} {26918642-829D-4FA2-B60A-BE8D83F4E063} = {C5A00AC3-B34C-4564-9BDD-2DA473EF4D8B} {11522B0D-BF31-42D5-8FC5-41E58F319AF9} = {C5A00AC3-B34C-4564-9BDD-2DA473EF4D8B} - {A7644C3B-B843-44F1-9940-560D56CB0936} = {4ED8B739-6F4E-4CD4-B993-545E6B5CE637} {FDFB9C25-552D-420B-9D4A-DB0BB6472239} = {4ED8B739-6F4E-4CD4-B993-545E6B5CE637} + {A7644C3B-B843-44F1-9940-560D56CB0936} = {4ED8B739-6F4E-4CD4-B993-545E6B5CE637} + {742C3613-514C-4D6B-804A-2A7925F278F3} = {86C53C40-57AA-45B8-AD42-FAE0EFDF0F2B} + {98A16FFD-0C99-4665-AC64-DC17E86879A2} = {4ED8B739-6F4E-4CD4-B993-545E6B5CE637} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {87366D66-1391-4D90-8999-95A620AD786A} diff --git a/Directory.Packages.props b/Directory.Packages.props new file mode 100644 index 0000000000..9441c68cac --- /dev/null +++ b/Directory.Packages.props @@ -0,0 +1,77 @@ + + + true + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/NOTICE.md b/NOTICE.md index 7083706c3e..365f5223fe 100644 --- a/NOTICE.md +++ b/NOTICE.md @@ -26,30 +26,6 @@ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -# SharpDX - -https://github.com/sharpdx/SharpDX - -Copyright (c) 2010-2014 SharpDX - Alexandre Mutel - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in -all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -THE SOFTWARE. - # Silverlight Toolkit https://github.com/microsoftarchive/SilverlightToolkit diff --git a/NuGet.Config b/NuGet.Config index 93d7ba8778..a065192b33 100644 --- a/NuGet.Config +++ b/NuGet.Config @@ -8,6 +8,12 @@ + + + + + + diff --git a/api/Avalonia.LinuxFramebuffer.nupkg.xml b/api/Avalonia.LinuxFramebuffer.nupkg.xml new file mode 100644 index 0000000000..0fa6ef4e03 --- /dev/null +++ b/api/Avalonia.LinuxFramebuffer.nupkg.xml @@ -0,0 +1,40 @@ + + + + + CP0002 + M:Avalonia.LinuxFramebuffer.FbdevOutput.CreateFramebufferRenderTarget + baseline/Avalonia.LinuxFramebuffer/lib/net10.0/Avalonia.LinuxFramebuffer.dll + current/Avalonia.LinuxFramebuffer/lib/net10.0/Avalonia.LinuxFramebuffer.dll + + + CP0002 + M:Avalonia.LinuxFramebuffer.FbdevOutput.Lock + baseline/Avalonia.LinuxFramebuffer/lib/net10.0/Avalonia.LinuxFramebuffer.dll + current/Avalonia.LinuxFramebuffer/lib/net10.0/Avalonia.LinuxFramebuffer.dll + + + CP0002 + M:Avalonia.LinuxFramebuffer.FbdevOutput.CreateFramebufferRenderTarget + baseline/Avalonia.LinuxFramebuffer/lib/net8.0/Avalonia.LinuxFramebuffer.dll + current/Avalonia.LinuxFramebuffer/lib/net8.0/Avalonia.LinuxFramebuffer.dll + + + CP0002 + M:Avalonia.LinuxFramebuffer.FbdevOutput.Lock + baseline/Avalonia.LinuxFramebuffer/lib/net8.0/Avalonia.LinuxFramebuffer.dll + current/Avalonia.LinuxFramebuffer/lib/net8.0/Avalonia.LinuxFramebuffer.dll + + + CP0008 + T:Avalonia.LinuxFramebuffer.FbdevOutput + baseline/Avalonia.LinuxFramebuffer/lib/net10.0/Avalonia.LinuxFramebuffer.dll + current/Avalonia.LinuxFramebuffer/lib/net10.0/Avalonia.LinuxFramebuffer.dll + + + CP0008 + T:Avalonia.LinuxFramebuffer.FbdevOutput + baseline/Avalonia.LinuxFramebuffer/lib/net8.0/Avalonia.LinuxFramebuffer.dll + current/Avalonia.LinuxFramebuffer/lib/net8.0/Avalonia.LinuxFramebuffer.dll + + diff --git a/api/Avalonia.Skia.nupkg.xml b/api/Avalonia.Skia.nupkg.xml index 8e9d60f7d4..b73745af8e 100644 --- a/api/Avalonia.Skia.nupkg.xml +++ b/api/Avalonia.Skia.nupkg.xml @@ -1,6 +1,12 @@ + + CP0001 + T:Avalonia.Skia.ISkiaGpu + baseline/Avalonia.Skia/lib/net10.0/Avalonia.Skia.dll + current/Avalonia.Skia/lib/net10.0/Avalonia.Skia.dll + CP0001 T:Avalonia.Skia.ISkiaGpuRenderTarget2 @@ -13,6 +19,12 @@ baseline/Avalonia.Skia/lib/net10.0/Avalonia.Skia.dll current/Avalonia.Skia/lib/net10.0/Avalonia.Skia.dll + + CP0001 + T:Avalonia.Skia.ISkiaGpu + baseline/Avalonia.Skia/lib/net8.0/Avalonia.Skia.dll + current/Avalonia.Skia/lib/net8.0/Avalonia.Skia.dll + CP0001 T:Avalonia.Skia.ISkiaGpuRenderTarget2 @@ -25,24 +37,72 @@ baseline/Avalonia.Skia/lib/net8.0/Avalonia.Skia.dll current/Avalonia.Skia/lib/net8.0/Avalonia.Skia.dll + + CP0002 + M:Avalonia.Skia.Helpers.DrawingContextHelper.WrapSkiaCanvas(SkiaSharp.SKCanvas,Avalonia.Vector) + baseline/Avalonia.Skia/lib/net10.0/Avalonia.Skia.dll + current/Avalonia.Skia/lib/net10.0/Avalonia.Skia.dll + + + CP0002 + M:Avalonia.Skia.ISkiaGpu.TryCreateRenderTarget(System.Collections.Generic.IEnumerable{System.Object}) + baseline/Avalonia.Skia/lib/net10.0/Avalonia.Skia.dll + current/Avalonia.Skia/lib/net10.0/Avalonia.Skia.dll + CP0002 M:Avalonia.Skia.ISkiaGpuRenderTarget.BeginRenderingSession baseline/Avalonia.Skia/lib/net10.0/Avalonia.Skia.dll current/Avalonia.Skia/lib/net10.0/Avalonia.Skia.dll + + CP0002 + M:Avalonia.Skia.ISkiaGpuRenderTarget.BeginRenderingSession(System.Nullable{Avalonia.PixelSize}) + baseline/Avalonia.Skia/lib/net10.0/Avalonia.Skia.dll + current/Avalonia.Skia/lib/net10.0/Avalonia.Skia.dll + + + CP0002 + M:Avalonia.Skia.Helpers.DrawingContextHelper.WrapSkiaCanvas(SkiaSharp.SKCanvas,Avalonia.Vector) + baseline/Avalonia.Skia/lib/net8.0/Avalonia.Skia.dll + current/Avalonia.Skia/lib/net8.0/Avalonia.Skia.dll + + + CP0002 + M:Avalonia.Skia.ISkiaGpu.TryCreateRenderTarget(System.Collections.Generic.IEnumerable{System.Object}) + baseline/Avalonia.Skia/lib/net8.0/Avalonia.Skia.dll + current/Avalonia.Skia/lib/net8.0/Avalonia.Skia.dll + CP0002 M:Avalonia.Skia.ISkiaGpuRenderTarget.BeginRenderingSession baseline/Avalonia.Skia/lib/net8.0/Avalonia.Skia.dll current/Avalonia.Skia/lib/net8.0/Avalonia.Skia.dll + + CP0002 + M:Avalonia.Skia.ISkiaGpuRenderTarget.BeginRenderingSession(System.Nullable{Avalonia.PixelSize}) + baseline/Avalonia.Skia/lib/net8.0/Avalonia.Skia.dll + current/Avalonia.Skia/lib/net8.0/Avalonia.Skia.dll + + + CP0006 + M:Avalonia.Skia.ISkiaGpu.TryCreateRenderTarget(System.Collections.Generic.IEnumerable{Avalonia.Platform.Surfaces.IPlatformRenderSurface}) + baseline/Avalonia.Skia/lib/net10.0/Avalonia.Skia.dll + current/Avalonia.Skia/lib/net10.0/Avalonia.Skia.dll + CP0006 M:Avalonia.Skia.ISkiaGpu.TryGetGrContext baseline/Avalonia.Skia/lib/net10.0/Avalonia.Skia.dll current/Avalonia.Skia/lib/net10.0/Avalonia.Skia.dll + + CP0006 + M:Avalonia.Skia.ISkiaGpuRenderTarget.BeginRenderingSession(Avalonia.Platform.IRenderTarget.RenderTargetSceneInfo) + baseline/Avalonia.Skia/lib/net10.0/Avalonia.Skia.dll + current/Avalonia.Skia/lib/net10.0/Avalonia.Skia.dll + CP0006 M:Avalonia.Skia.ISkiaGpuRenderTarget.BeginRenderingSession(System.Nullable{Avalonia.PixelSize}) @@ -55,12 +115,24 @@ baseline/Avalonia.Skia/lib/net10.0/Avalonia.Skia.dll current/Avalonia.Skia/lib/net10.0/Avalonia.Skia.dll + + CP0006 + M:Avalonia.Skia.ISkiaGpu.TryCreateRenderTarget(System.Collections.Generic.IEnumerable{Avalonia.Platform.Surfaces.IPlatformRenderSurface}) + baseline/Avalonia.Skia/lib/net8.0/Avalonia.Skia.dll + current/Avalonia.Skia/lib/net8.0/Avalonia.Skia.dll + CP0006 M:Avalonia.Skia.ISkiaGpu.TryGetGrContext baseline/Avalonia.Skia/lib/net8.0/Avalonia.Skia.dll current/Avalonia.Skia/lib/net8.0/Avalonia.Skia.dll + + CP0006 + M:Avalonia.Skia.ISkiaGpuRenderTarget.BeginRenderingSession(Avalonia.Platform.IRenderTarget.RenderTargetSceneInfo) + baseline/Avalonia.Skia/lib/net8.0/Avalonia.Skia.dll + current/Avalonia.Skia/lib/net8.0/Avalonia.Skia.dll + CP0006 M:Avalonia.Skia.ISkiaGpuRenderTarget.BeginRenderingSession(System.Nullable{Avalonia.PixelSize}) diff --git a/api/Avalonia.nupkg.xml b/api/Avalonia.nupkg.xml index 838d2bd70b..b2a81dd55d 100644 --- a/api/Avalonia.nupkg.xml +++ b/api/Avalonia.nupkg.xml @@ -103,18 +103,72 @@ baseline/Avalonia/lib/net10.0/Avalonia.Base.dll current/Avalonia/lib/net10.0/Avalonia.Base.dll + + CP0001 + T:Avalonia.Input.Gestures + baseline/Avalonia/lib/net10.0/Avalonia.Base.dll + current/Avalonia/lib/net10.0/Avalonia.Base.dll + + + CP0001 + T:Avalonia.Input.GotFocusEventArgs + baseline/Avalonia/lib/net10.0/Avalonia.Base.dll + current/Avalonia/lib/net10.0/Avalonia.Base.dll + CP0001 T:Avalonia.Input.IDataObject baseline/Avalonia/lib/net10.0/Avalonia.Base.dll current/Avalonia/lib/net10.0/Avalonia.Base.dll + + CP0001 + T:Avalonia.Input.IKeyboardNavigationHandler + baseline/Avalonia/lib/net10.0/Avalonia.Base.dll + current/Avalonia/lib/net10.0/Avalonia.Base.dll + + + CP0001 + T:Avalonia.Input.KeyboardNavigationHandler + baseline/Avalonia/lib/net10.0/Avalonia.Base.dll + current/Avalonia/lib/net10.0/Avalonia.Base.dll + + + CP0001 + T:Avalonia.Input.TextInput.ITextInputMethodRoot + baseline/Avalonia/lib/net10.0/Avalonia.Base.dll + current/Avalonia/lib/net10.0/Avalonia.Base.dll + + + CP0001 + T:Avalonia.Layout.IEmbeddedLayoutRoot + baseline/Avalonia/lib/net10.0/Avalonia.Base.dll + current/Avalonia/lib/net10.0/Avalonia.Base.dll + + + CP0001 + T:Avalonia.Layout.ILayoutRoot + baseline/Avalonia/lib/net10.0/Avalonia.Base.dll + current/Avalonia/lib/net10.0/Avalonia.Base.dll + + + CP0001 + T:Avalonia.Layout.LayoutManager + baseline/Avalonia/lib/net10.0/Avalonia.Base.dll + current/Avalonia/lib/net10.0/Avalonia.Base.dll + CP0001 T:Avalonia.Media.Fonts.FontFamilyLoader baseline/Avalonia/lib/net10.0/Avalonia.Base.dll current/Avalonia/lib/net10.0/Avalonia.Base.dll + + CP0001 + T:Avalonia.Media.TextFormatting.TextRange + baseline/Avalonia/lib/net10.0/Avalonia.Base.dll + current/Avalonia/lib/net10.0/Avalonia.Base.dll + CP0001 T:Avalonia.Platform.IGeometryContext2 @@ -151,18 +205,48 @@ baseline/Avalonia/lib/net10.0/Avalonia.Base.dll current/Avalonia/lib/net10.0/Avalonia.Base.dll + + CP0001 + T:Avalonia.Rendering.IHitTester + baseline/Avalonia/lib/net10.0/Avalonia.Base.dll + current/Avalonia/lib/net10.0/Avalonia.Base.dll + + + CP0001 + T:Avalonia.Rendering.IRenderer + baseline/Avalonia/lib/net10.0/Avalonia.Base.dll + current/Avalonia/lib/net10.0/Avalonia.Base.dll + + + CP0001 + T:Avalonia.Rendering.IRenderRoot + baseline/Avalonia/lib/net10.0/Avalonia.Base.dll + current/Avalonia/lib/net10.0/Avalonia.Base.dll + CP0001 T:Avalonia.Styling.IStyleable baseline/Avalonia/lib/net10.0/Avalonia.Base.dll current/Avalonia/lib/net10.0/Avalonia.Base.dll + + CP0001 + T:Avalonia.Utilities.MathUtilities + baseline/Avalonia/lib/net10.0/Avalonia.Base.dll + current/Avalonia/lib/net10.0/Avalonia.Base.dll + CP0001 T:Avalonia.Utilities.StringTokenizer baseline/Avalonia/lib/net10.0/Avalonia.Base.dll current/Avalonia/lib/net10.0/Avalonia.Base.dll + + CP0001 + T:Avalonia.VisualTree.IHostedVisualTreeRoot + baseline/Avalonia/lib/net10.0/Avalonia.Base.dll + current/Avalonia/lib/net10.0/Avalonia.Base.dll + CP0001 T:Avalonia.Controls.ApplicationLifetimes.ClassicDesktopStyleApplicationLifetimeOptions @@ -181,6 +265,30 @@ baseline/Avalonia/lib/net10.0/Avalonia.Controls.dll current/Avalonia/lib/net10.0/Avalonia.Controls.dll + + CP0001 + T:Avalonia.Controls.Chrome.CaptionButtons + baseline/Avalonia/lib/net10.0/Avalonia.Controls.dll + current/Avalonia/lib/net10.0/Avalonia.Controls.dll + + + CP0001 + T:Avalonia.Controls.Chrome.TitleBar + baseline/Avalonia/lib/net10.0/Avalonia.Controls.dll + current/Avalonia/lib/net10.0/Avalonia.Controls.dll + + + CP0001 + T:Avalonia.Controls.ContextRequestedEventArgs + baseline/Avalonia/lib/net10.0/Avalonia.Controls.dll + current/Avalonia/lib/net10.0/Avalonia.Controls.dll + + + CP0001 + T:Avalonia.Controls.Diagnostics.IPopupHostProvider + baseline/Avalonia/lib/net10.0/Avalonia.Controls.dll + current/Avalonia/lib/net10.0/Avalonia.Controls.dll + CP0001 T:Avalonia.Controls.FileDialog @@ -235,24 +343,114 @@ baseline/Avalonia/lib/net10.0/Avalonia.Controls.dll current/Avalonia/lib/net10.0/Avalonia.Controls.dll + + CP0001 + T:Avalonia.Controls.Platform.ManagedDispatcherImpl + baseline/Avalonia/lib/net10.0/Avalonia.Controls.dll + current/Avalonia/lib/net10.0/Avalonia.Controls.dll + + + CP0001 + T:Avalonia.Controls.Platform.Surfaces.FramebufferLockProperties + baseline/Avalonia/lib/net10.0/Avalonia.Controls.dll + current/Avalonia/lib/net10.0/Avalonia.Controls.dll + + + CP0001 + T:Avalonia.Controls.Platform.Surfaces.FuncFramebufferRenderTarget + baseline/Avalonia/lib/net10.0/Avalonia.Controls.dll + current/Avalonia/lib/net10.0/Avalonia.Controls.dll + + + CP0001 + T:Avalonia.Controls.Platform.Surfaces.IFramebufferPlatformSurface + baseline/Avalonia/lib/net10.0/Avalonia.Controls.dll + current/Avalonia/lib/net10.0/Avalonia.Controls.dll + + + CP0001 + T:Avalonia.Controls.Platform.Surfaces.IFramebufferRenderTarget + baseline/Avalonia/lib/net10.0/Avalonia.Controls.dll + current/Avalonia/lib/net10.0/Avalonia.Controls.dll + + + CP0001 + T:Avalonia.Controls.Platform.Surfaces.IFramebufferRenderTargetWithProperties + baseline/Avalonia/lib/net10.0/Avalonia.Controls.dll + current/Avalonia/lib/net10.0/Avalonia.Controls.dll + + + CP0001 + T:Avalonia.Controls.Primitives.ChromeOverlayLayer + baseline/Avalonia/lib/net10.0/Avalonia.Controls.dll + current/Avalonia/lib/net10.0/Avalonia.Controls.dll + + + CP0001 + T:Avalonia.Controls.Primitives.IPopupHost + baseline/Avalonia/lib/net10.0/Avalonia.Controls.dll + current/Avalonia/lib/net10.0/Avalonia.Controls.dll + CP0001 T:Avalonia.Controls.Primitives.IScrollable baseline/Avalonia/lib/net10.0/Avalonia.Controls.dll current/Avalonia/lib/net10.0/Avalonia.Controls.dll + + CP0001 + T:Avalonia.Controls.Primitives.LightDismissOverlayLayer + baseline/Avalonia/lib/net10.0/Avalonia.Controls.dll + current/Avalonia/lib/net10.0/Avalonia.Controls.dll + + + CP0001 + T:Avalonia.Controls.Primitives.OverlayLayer + baseline/Avalonia/lib/net10.0/Avalonia.Controls.dll + current/Avalonia/lib/net10.0/Avalonia.Controls.dll + + + CP0001 + T:Avalonia.Controls.Primitives.SelectionHandleType + baseline/Avalonia/lib/net10.0/Avalonia.Controls.dll + current/Avalonia/lib/net10.0/Avalonia.Controls.dll + + + CP0001 + T:Avalonia.Controls.Remote.RemoteServer + baseline/Avalonia/lib/net10.0/Avalonia.Controls.dll + current/Avalonia/lib/net10.0/Avalonia.Controls.dll + + + CP0001 + T:Avalonia.Controls.Remote.RemoteWidget + baseline/Avalonia/lib/net10.0/Avalonia.Controls.dll + current/Avalonia/lib/net10.0/Avalonia.Controls.dll + CP0001 T:Avalonia.Controls.SaveFileDialog baseline/Avalonia/lib/net10.0/Avalonia.Controls.dll current/Avalonia/lib/net10.0/Avalonia.Controls.dll + + CP0001 + T:Avalonia.Controls.SystemDecorations + baseline/Avalonia/lib/net10.0/Avalonia.Controls.dll + current/Avalonia/lib/net10.0/Avalonia.Controls.dll + CP0001 T:Avalonia.Controls.SystemDialog baseline/Avalonia/lib/net10.0/Avalonia.Controls.dll current/Avalonia/lib/net10.0/Avalonia.Controls.dll + + CP0001 + T:Avalonia.Platform.ExtendClientAreaChromeHints + baseline/Avalonia/lib/net10.0/Avalonia.Controls.dll + current/Avalonia/lib/net10.0/Avalonia.Controls.dll + CP0001 T:Avalonia.Platform.IApplicationPlatformEvents @@ -385,18 +583,72 @@ baseline/Avalonia/lib/net8.0/Avalonia.Base.dll current/Avalonia/lib/net8.0/Avalonia.Base.dll + + CP0001 + T:Avalonia.Input.Gestures + baseline/Avalonia/lib/net8.0/Avalonia.Base.dll + current/Avalonia/lib/net8.0/Avalonia.Base.dll + + + CP0001 + T:Avalonia.Input.GotFocusEventArgs + baseline/Avalonia/lib/net8.0/Avalonia.Base.dll + current/Avalonia/lib/net8.0/Avalonia.Base.dll + CP0001 T:Avalonia.Input.IDataObject baseline/Avalonia/lib/net8.0/Avalonia.Base.dll current/Avalonia/lib/net8.0/Avalonia.Base.dll + + CP0001 + T:Avalonia.Input.IKeyboardNavigationHandler + baseline/Avalonia/lib/net8.0/Avalonia.Base.dll + current/Avalonia/lib/net8.0/Avalonia.Base.dll + + + CP0001 + T:Avalonia.Input.KeyboardNavigationHandler + baseline/Avalonia/lib/net8.0/Avalonia.Base.dll + current/Avalonia/lib/net8.0/Avalonia.Base.dll + + + CP0001 + T:Avalonia.Input.TextInput.ITextInputMethodRoot + baseline/Avalonia/lib/net8.0/Avalonia.Base.dll + current/Avalonia/lib/net8.0/Avalonia.Base.dll + + + CP0001 + T:Avalonia.Layout.IEmbeddedLayoutRoot + baseline/Avalonia/lib/net8.0/Avalonia.Base.dll + current/Avalonia/lib/net8.0/Avalonia.Base.dll + + + CP0001 + T:Avalonia.Layout.ILayoutRoot + baseline/Avalonia/lib/net8.0/Avalonia.Base.dll + current/Avalonia/lib/net8.0/Avalonia.Base.dll + + + CP0001 + T:Avalonia.Layout.LayoutManager + baseline/Avalonia/lib/net8.0/Avalonia.Base.dll + current/Avalonia/lib/net8.0/Avalonia.Base.dll + CP0001 T:Avalonia.Media.Fonts.FontFamilyLoader baseline/Avalonia/lib/net8.0/Avalonia.Base.dll current/Avalonia/lib/net8.0/Avalonia.Base.dll + + CP0001 + T:Avalonia.Media.TextFormatting.TextRange + baseline/Avalonia/lib/net8.0/Avalonia.Base.dll + current/Avalonia/lib/net8.0/Avalonia.Base.dll + CP0001 T:Avalonia.Platform.IGeometryContext2 @@ -433,18 +685,48 @@ baseline/Avalonia/lib/net8.0/Avalonia.Base.dll current/Avalonia/lib/net8.0/Avalonia.Base.dll + + CP0001 + T:Avalonia.Rendering.IHitTester + baseline/Avalonia/lib/net8.0/Avalonia.Base.dll + current/Avalonia/lib/net8.0/Avalonia.Base.dll + + + CP0001 + T:Avalonia.Rendering.IRenderer + baseline/Avalonia/lib/net8.0/Avalonia.Base.dll + current/Avalonia/lib/net8.0/Avalonia.Base.dll + + + CP0001 + T:Avalonia.Rendering.IRenderRoot + baseline/Avalonia/lib/net8.0/Avalonia.Base.dll + current/Avalonia/lib/net8.0/Avalonia.Base.dll + CP0001 T:Avalonia.Styling.IStyleable baseline/Avalonia/lib/net8.0/Avalonia.Base.dll current/Avalonia/lib/net8.0/Avalonia.Base.dll + + CP0001 + T:Avalonia.Utilities.MathUtilities + baseline/Avalonia/lib/net8.0/Avalonia.Base.dll + current/Avalonia/lib/net8.0/Avalonia.Base.dll + CP0001 T:Avalonia.Utilities.StringTokenizer baseline/Avalonia/lib/net8.0/Avalonia.Base.dll current/Avalonia/lib/net8.0/Avalonia.Base.dll + + CP0001 + T:Avalonia.VisualTree.IHostedVisualTreeRoot + baseline/Avalonia/lib/net8.0/Avalonia.Base.dll + current/Avalonia/lib/net8.0/Avalonia.Base.dll + CP0001 T:Avalonia.Controls.ApplicationLifetimes.ClassicDesktopStyleApplicationLifetimeOptions @@ -465,79 +747,193 @@ CP0001 - T:Avalonia.Controls.FileDialog + T:Avalonia.Controls.Chrome.CaptionButtons baseline/Avalonia/lib/net8.0/Avalonia.Controls.dll current/Avalonia/lib/net8.0/Avalonia.Controls.dll CP0001 - T:Avalonia.Controls.FileDialogFilter + T:Avalonia.Controls.Chrome.TitleBar baseline/Avalonia/lib/net8.0/Avalonia.Controls.dll current/Avalonia/lib/net8.0/Avalonia.Controls.dll CP0001 - T:Avalonia.Controls.FileSystemDialog + T:Avalonia.Controls.ContextRequestedEventArgs baseline/Avalonia/lib/net8.0/Avalonia.Controls.dll current/Avalonia/lib/net8.0/Avalonia.Controls.dll CP0001 - T:Avalonia.Controls.Generators.TreeContainerIndex + T:Avalonia.Controls.Diagnostics.IPopupHostProvider baseline/Avalonia/lib/net8.0/Avalonia.Controls.dll current/Avalonia/lib/net8.0/Avalonia.Controls.dll CP0001 - T:Avalonia.Controls.Generators.TreeItemContainerGenerator + T:Avalonia.Controls.FileDialog baseline/Avalonia/lib/net8.0/Avalonia.Controls.dll current/Avalonia/lib/net8.0/Avalonia.Controls.dll CP0001 - T:Avalonia.Controls.NativeMenuItemToggleType + T:Avalonia.Controls.FileDialogFilter baseline/Avalonia/lib/net8.0/Avalonia.Controls.dll current/Avalonia/lib/net8.0/Avalonia.Controls.dll CP0001 - T:Avalonia.Controls.OpenFileDialog + T:Avalonia.Controls.FileSystemDialog baseline/Avalonia/lib/net8.0/Avalonia.Controls.dll current/Avalonia/lib/net8.0/Avalonia.Controls.dll CP0001 - T:Avalonia.Controls.OpenFolderDialog + T:Avalonia.Controls.Generators.TreeContainerIndex baseline/Avalonia/lib/net8.0/Avalonia.Controls.dll current/Avalonia/lib/net8.0/Avalonia.Controls.dll CP0001 - T:Avalonia.Controls.Platform.ISystemDialogImpl + T:Avalonia.Controls.Generators.TreeItemContainerGenerator baseline/Avalonia/lib/net8.0/Avalonia.Controls.dll current/Avalonia/lib/net8.0/Avalonia.Controls.dll CP0001 - T:Avalonia.Controls.Primitives.IScrollable + T:Avalonia.Controls.NativeMenuItemToggleType baseline/Avalonia/lib/net8.0/Avalonia.Controls.dll current/Avalonia/lib/net8.0/Avalonia.Controls.dll CP0001 - T:Avalonia.Controls.SaveFileDialog + T:Avalonia.Controls.OpenFileDialog baseline/Avalonia/lib/net8.0/Avalonia.Controls.dll current/Avalonia/lib/net8.0/Avalonia.Controls.dll CP0001 - T:Avalonia.Controls.SystemDialog + T:Avalonia.Controls.OpenFolderDialog baseline/Avalonia/lib/net8.0/Avalonia.Controls.dll current/Avalonia/lib/net8.0/Avalonia.Controls.dll CP0001 - T:Avalonia.Platform.IApplicationPlatformEvents + T:Avalonia.Controls.Platform.ISystemDialogImpl + baseline/Avalonia/lib/net8.0/Avalonia.Controls.dll + current/Avalonia/lib/net8.0/Avalonia.Controls.dll + + + CP0001 + T:Avalonia.Controls.Platform.ManagedDispatcherImpl + baseline/Avalonia/lib/net8.0/Avalonia.Controls.dll + current/Avalonia/lib/net8.0/Avalonia.Controls.dll + + + CP0001 + T:Avalonia.Controls.Platform.Surfaces.FramebufferLockProperties + baseline/Avalonia/lib/net8.0/Avalonia.Controls.dll + current/Avalonia/lib/net8.0/Avalonia.Controls.dll + + + CP0001 + T:Avalonia.Controls.Platform.Surfaces.FuncFramebufferRenderTarget + baseline/Avalonia/lib/net8.0/Avalonia.Controls.dll + current/Avalonia/lib/net8.0/Avalonia.Controls.dll + + + CP0001 + T:Avalonia.Controls.Platform.Surfaces.IFramebufferPlatformSurface + baseline/Avalonia/lib/net8.0/Avalonia.Controls.dll + current/Avalonia/lib/net8.0/Avalonia.Controls.dll + + + CP0001 + T:Avalonia.Controls.Platform.Surfaces.IFramebufferRenderTarget + baseline/Avalonia/lib/net8.0/Avalonia.Controls.dll + current/Avalonia/lib/net8.0/Avalonia.Controls.dll + + + CP0001 + T:Avalonia.Controls.Platform.Surfaces.IFramebufferRenderTargetWithProperties + baseline/Avalonia/lib/net8.0/Avalonia.Controls.dll + current/Avalonia/lib/net8.0/Avalonia.Controls.dll + + + CP0001 + T:Avalonia.Controls.Primitives.ChromeOverlayLayer + baseline/Avalonia/lib/net8.0/Avalonia.Controls.dll + current/Avalonia/lib/net8.0/Avalonia.Controls.dll + + + CP0001 + T:Avalonia.Controls.Primitives.IPopupHost + baseline/Avalonia/lib/net8.0/Avalonia.Controls.dll + current/Avalonia/lib/net8.0/Avalonia.Controls.dll + + + CP0001 + T:Avalonia.Controls.Primitives.IScrollable + baseline/Avalonia/lib/net8.0/Avalonia.Controls.dll + current/Avalonia/lib/net8.0/Avalonia.Controls.dll + + + CP0001 + T:Avalonia.Controls.Primitives.LightDismissOverlayLayer + baseline/Avalonia/lib/net8.0/Avalonia.Controls.dll + current/Avalonia/lib/net8.0/Avalonia.Controls.dll + + + CP0001 + T:Avalonia.Controls.Primitives.OverlayLayer + baseline/Avalonia/lib/net8.0/Avalonia.Controls.dll + current/Avalonia/lib/net8.0/Avalonia.Controls.dll + + + CP0001 + T:Avalonia.Controls.Primitives.SelectionHandleType + baseline/Avalonia/lib/net8.0/Avalonia.Controls.dll + current/Avalonia/lib/net8.0/Avalonia.Controls.dll + + + CP0001 + T:Avalonia.Controls.Remote.RemoteServer + baseline/Avalonia/lib/net8.0/Avalonia.Controls.dll + current/Avalonia/lib/net8.0/Avalonia.Controls.dll + + + CP0001 + T:Avalonia.Controls.Remote.RemoteWidget + baseline/Avalonia/lib/net8.0/Avalonia.Controls.dll + current/Avalonia/lib/net8.0/Avalonia.Controls.dll + + + CP0001 + T:Avalonia.Controls.SaveFileDialog + baseline/Avalonia/lib/net8.0/Avalonia.Controls.dll + current/Avalonia/lib/net8.0/Avalonia.Controls.dll + + + CP0001 + T:Avalonia.Controls.SystemDecorations + baseline/Avalonia/lib/net8.0/Avalonia.Controls.dll + current/Avalonia/lib/net8.0/Avalonia.Controls.dll + + + CP0001 + T:Avalonia.Controls.SystemDialog + baseline/Avalonia/lib/net8.0/Avalonia.Controls.dll + current/Avalonia/lib/net8.0/Avalonia.Controls.dll + + + CP0001 + T:Avalonia.Platform.ExtendClientAreaChromeHints + baseline/Avalonia/lib/net8.0/Avalonia.Controls.dll + current/Avalonia/lib/net8.0/Avalonia.Controls.dll + + + CP0001 + T:Avalonia.Platform.IApplicationPlatformEvents baseline/Avalonia/lib/net8.0/Avalonia.Controls.dll current/Avalonia/lib/net8.0/Avalonia.Controls.dll @@ -595,6 +991,36 @@ baseline/Avalonia/lib/net10.0/Avalonia.Base.dll current/Avalonia/lib/net10.0/Avalonia.Base.dll + + CP0002 + F:Avalonia.Input.GestureRecognizers.SwipeGestureRecognizer.CrossAxisCancelThresholdProperty + baseline/Avalonia/lib/net10.0/Avalonia.Base.dll + current/Avalonia/lib/net10.0/Avalonia.Base.dll + + + CP0002 + F:Avalonia.Input.GestureRecognizers.SwipeGestureRecognizer.EdgeSizeProperty + baseline/Avalonia/lib/net10.0/Avalonia.Base.dll + current/Avalonia/lib/net10.0/Avalonia.Base.dll + + + CP0002 + F:Avalonia.Input.HoldingState.Cancelled + baseline/Avalonia/lib/net10.0/Avalonia.Base.dll + current/Avalonia/lib/net10.0/Avalonia.Base.dll + + + CP0002 + F:Avalonia.Input.InputElement.GotFocusEvent + baseline/Avalonia/lib/net10.0/Avalonia.Base.dll + current/Avalonia/lib/net10.0/Avalonia.Base.dll + + + CP0002 + F:Avalonia.Input.InputElement.LostFocusEvent + baseline/Avalonia/lib/net10.0/Avalonia.Base.dll + current/Avalonia/lib/net10.0/Avalonia.Base.dll + CP0002 F:Avalonia.Media.DrawingImage.ViewboxProperty @@ -637,6 +1063,24 @@ baseline/Avalonia/lib/net10.0/Avalonia.Base.dll current/Avalonia/lib/net10.0/Avalonia.Base.dll + + CP0002 + M:Avalonia.Data.CompiledBindingPathBuilder.StreamObservable + baseline/Avalonia/lib/net10.0/Avalonia.Base.dll + current/Avalonia/lib/net10.0/Avalonia.Base.dll + + + CP0002 + M:Avalonia.Data.CompiledBindingPathBuilder.StreamTask + baseline/Avalonia/lib/net10.0/Avalonia.Base.dll + current/Avalonia/lib/net10.0/Avalonia.Base.dll + + + CP0002 + M:Avalonia.Data.CompiledBindingPathBuilder.TypeCast(System.Type) + baseline/Avalonia/lib/net10.0/Avalonia.Base.dll + current/Avalonia/lib/net10.0/Avalonia.Base.dll + CP0002 M:Avalonia.Data.ReflectionBinding.#ctor(System.String,Avalonia.Data.BindingMode) @@ -687,1237 +1131,2365 @@ CP0002 - M:Avalonia.Input.IKeyboardNavigationHandler.Move(Avalonia.Input.IInputElement,Avalonia.Input.NavigationDirection,Avalonia.Input.KeyModifiers) + M:Avalonia.Input.FocusManager.#ctor(Avalonia.Input.IInputElement) baseline/Avalonia/lib/net10.0/Avalonia.Base.dll current/Avalonia/lib/net10.0/Avalonia.Base.dll CP0002 - M:Avalonia.Input.KeyboardNavigationHandler.Move(Avalonia.Input.IInputElement,Avalonia.Input.NavigationDirection,Avalonia.Input.KeyModifiers) + M:Avalonia.Input.FocusManager.ClearFocus baseline/Avalonia/lib/net10.0/Avalonia.Base.dll current/Avalonia/lib/net10.0/Avalonia.Base.dll CP0002 - M:Avalonia.Input.Platform.IClipboard.GetDataAsync(System.String) + M:Avalonia.Input.FocusManager.ClearFocusOnElementRemoved(Avalonia.Input.IInputElement,Avalonia.Visual) baseline/Avalonia/lib/net10.0/Avalonia.Base.dll current/Avalonia/lib/net10.0/Avalonia.Base.dll CP0002 - M:Avalonia.Input.Platform.IClipboard.GetFormatsAsync + M:Avalonia.Input.FocusManager.FindNextElement(Avalonia.Input.NavigationDirection) baseline/Avalonia/lib/net10.0/Avalonia.Base.dll current/Avalonia/lib/net10.0/Avalonia.Base.dll CP0002 - M:Avalonia.Input.Platform.IClipboard.GetTextAsync + M:Avalonia.Input.FocusManager.TryMoveFocus(Avalonia.Input.NavigationDirection) baseline/Avalonia/lib/net10.0/Avalonia.Base.dll current/Avalonia/lib/net10.0/Avalonia.Base.dll CP0002 - M:Avalonia.Input.Platform.IClipboard.SetDataObjectAsync(Avalonia.Input.IDataObject) + M:Avalonia.Input.GestureRecognizers.SwipeGestureRecognizer.get_CrossAxisCancelThreshold baseline/Avalonia/lib/net10.0/Avalonia.Base.dll current/Avalonia/lib/net10.0/Avalonia.Base.dll CP0002 - M:Avalonia.Input.Platform.IClipboard.SetTextAsync(System.String) + M:Avalonia.Input.GestureRecognizers.SwipeGestureRecognizer.get_EdgeSize baseline/Avalonia/lib/net10.0/Avalonia.Base.dll current/Avalonia/lib/net10.0/Avalonia.Base.dll CP0002 - M:Avalonia.Input.Platform.IClipboard.TryGetInProcessDataObjectAsync + M:Avalonia.Input.GestureRecognizers.SwipeGestureRecognizer.set_CrossAxisCancelThreshold(System.Double) baseline/Avalonia/lib/net10.0/Avalonia.Base.dll current/Avalonia/lib/net10.0/Avalonia.Base.dll CP0002 - M:Avalonia.Input.Platform.IPlatformDragSource.DoDragDrop(Avalonia.Input.PointerEventArgs,Avalonia.Input.IDataObject,Avalonia.Input.DragDropEffects) + M:Avalonia.Input.GestureRecognizers.SwipeGestureRecognizer.set_EdgeSize(System.Double) baseline/Avalonia/lib/net10.0/Avalonia.Base.dll current/Avalonia/lib/net10.0/Avalonia.Base.dll CP0002 - M:Avalonia.Input.Raw.RawDragEvent.#ctor(Avalonia.Input.Raw.IDragDropDevice,Avalonia.Input.Raw.RawDragEventType,Avalonia.Input.IInputRoot,Avalonia.Point,Avalonia.Input.IDataObject,Avalonia.Input.DragDropEffects,Avalonia.Input.RawInputModifiers) + M:Avalonia.Input.HoldingRoutedEventArgs.#ctor(Avalonia.Input.HoldingState,Avalonia.Point,Avalonia.Input.PointerType) baseline/Avalonia/lib/net10.0/Avalonia.Base.dll current/Avalonia/lib/net10.0/Avalonia.Base.dll CP0002 - M:Avalonia.Input.Raw.RawDragEvent.get_Data + M:Avalonia.Input.IFocusManager.ClearFocus baseline/Avalonia/lib/net10.0/Avalonia.Base.dll current/Avalonia/lib/net10.0/Avalonia.Base.dll CP0002 - M:Avalonia.Input.Raw.RawKeyEventArgs.#ctor(Avalonia.Input.IInputDevice,System.UInt64,Avalonia.Input.IInputRoot,Avalonia.Input.Raw.RawKeyEventType,Avalonia.Input.Key,Avalonia.Input.RawInputModifiers,Avalonia.Input.PhysicalKey,Avalonia.Input.KeyDeviceType,System.String) + M:Avalonia.Input.IInputRoot.get_KeyboardNavigationHandler baseline/Avalonia/lib/net10.0/Avalonia.Base.dll current/Avalonia/lib/net10.0/Avalonia.Base.dll CP0002 - M:Avalonia.Input.Raw.RawKeyEventArgs.#ctor(Avalonia.Input.IInputDevice,System.UInt64,Avalonia.Input.IInputRoot,Avalonia.Input.Raw.RawKeyEventType,Avalonia.Input.Key,Avalonia.Input.RawInputModifiers,Avalonia.Input.PhysicalKey,System.String) + M:Avalonia.Input.IInputRoot.get_PlatformSettings baseline/Avalonia/lib/net10.0/Avalonia.Base.dll current/Avalonia/lib/net10.0/Avalonia.Base.dll CP0002 - M:Avalonia.Input.Raw.RawKeyEventArgs.#ctor(Avalonia.Input.IKeyboardDevice,System.UInt64,Avalonia.Input.IInputRoot,Avalonia.Input.Raw.RawKeyEventType,Avalonia.Input.Key,Avalonia.Input.RawInputModifiers) + M:Avalonia.Input.IInputRoot.get_PointerOverElement baseline/Avalonia/lib/net10.0/Avalonia.Base.dll current/Avalonia/lib/net10.0/Avalonia.Base.dll CP0002 - M:Avalonia.Input.TextInput.TextInputMethodClient.ShowInputPanel + M:Avalonia.Input.IInputRoot.get_ShowAccessKeys baseline/Avalonia/lib/net10.0/Avalonia.Base.dll current/Avalonia/lib/net10.0/Avalonia.Base.dll CP0002 - M:Avalonia.Media.Color.ToUint32 + M:Avalonia.Input.IInputRoot.set_PointerOverElement(Avalonia.Input.IInputElement) baseline/Avalonia/lib/net10.0/Avalonia.Base.dll current/Avalonia/lib/net10.0/Avalonia.Base.dll CP0002 - M:Avalonia.Media.DrawingContext.PushPostTransform(Avalonia.Matrix) + M:Avalonia.Input.IInputRoot.set_ShowAccessKeys(System.Boolean) baseline/Avalonia/lib/net10.0/Avalonia.Base.dll current/Avalonia/lib/net10.0/Avalonia.Base.dll CP0002 - M:Avalonia.Media.DrawingContext.PushPreTransform(Avalonia.Matrix) + M:Avalonia.Input.IKeyboardNavigationHandler.Move(Avalonia.Input.IInputElement,Avalonia.Input.NavigationDirection,Avalonia.Input.KeyModifiers) baseline/Avalonia/lib/net10.0/Avalonia.Base.dll current/Avalonia/lib/net10.0/Avalonia.Base.dll CP0002 - M:Avalonia.Media.DrawingContext.PushTransformContainer + M:Avalonia.Input.InputElement.AddPinchEndedHandler(Avalonia.Interactivity.Interactive,System.EventHandler{Avalonia.Input.PinchEndedEventArgs}) baseline/Avalonia/lib/net10.0/Avalonia.Base.dll current/Avalonia/lib/net10.0/Avalonia.Base.dll CP0002 - M:Avalonia.Media.DrawingImage.get_Viewbox + M:Avalonia.Input.InputElement.AddPinchHandler(Avalonia.Interactivity.Interactive,System.EventHandler{Avalonia.Input.PinchEventArgs}) baseline/Avalonia/lib/net10.0/Avalonia.Base.dll current/Avalonia/lib/net10.0/Avalonia.Base.dll CP0002 - M:Avalonia.Media.DrawingImage.set_Viewbox(System.Nullable{Avalonia.Rect}) + M:Avalonia.Input.InputElement.AddPointerTouchPadGestureMagnifyHandler(Avalonia.Interactivity.Interactive,System.EventHandler{Avalonia.Input.PointerDeltaEventArgs}) baseline/Avalonia/lib/net10.0/Avalonia.Base.dll current/Avalonia/lib/net10.0/Avalonia.Base.dll CP0002 - M:Avalonia.Media.Fonts.FontCollectionBase.Initialize(Avalonia.Platform.IFontManagerImpl) + M:Avalonia.Input.InputElement.AddPointerTouchPadGestureRotateHandler(Avalonia.Interactivity.Interactive,System.EventHandler{Avalonia.Input.PointerDeltaEventArgs}) baseline/Avalonia/lib/net10.0/Avalonia.Base.dll current/Avalonia/lib/net10.0/Avalonia.Base.dll CP0002 - M:Avalonia.Media.Fonts.IFontCollection.Initialize(Avalonia.Platform.IFontManagerImpl) + M:Avalonia.Input.InputElement.AddPointerTouchPadGestureSwipeHandler(Avalonia.Interactivity.Interactive,System.EventHandler{Avalonia.Input.PointerDeltaEventArgs}) baseline/Avalonia/lib/net10.0/Avalonia.Base.dll current/Avalonia/lib/net10.0/Avalonia.Base.dll CP0002 - M:Avalonia.Media.Imaging.Bitmap.CopyPixels(Avalonia.Platform.ILockedFramebuffer,Avalonia.Platform.AlphaFormat) + M:Avalonia.Input.InputElement.AddPullGestureEndedHandler(Avalonia.Interactivity.Interactive,System.EventHandler{Avalonia.Input.PullGestureEndedEventArgs}) baseline/Avalonia/lib/net10.0/Avalonia.Base.dll current/Avalonia/lib/net10.0/Avalonia.Base.dll CP0002 - M:Avalonia.Media.Immutable.ImmutableRadialGradientBrush.get_Radius + M:Avalonia.Input.InputElement.AddPullGestureHandler(Avalonia.Interactivity.Interactive,System.EventHandler{Avalonia.Input.PullGestureEventArgs}) baseline/Avalonia/lib/net10.0/Avalonia.Base.dll current/Avalonia/lib/net10.0/Avalonia.Base.dll CP0002 - M:Avalonia.Media.IRadialGradientBrush.get_Radius + M:Avalonia.Input.InputElement.AddScrollGestureEndedHandler(Avalonia.Interactivity.Interactive,System.EventHandler{Avalonia.Input.ScrollGestureEndedEventArgs}) baseline/Avalonia/lib/net10.0/Avalonia.Base.dll current/Avalonia/lib/net10.0/Avalonia.Base.dll CP0002 - M:Avalonia.Media.RadialGradientBrush.get_Radius + M:Avalonia.Input.InputElement.AddScrollGestureHandler(Avalonia.Interactivity.Interactive,System.EventHandler{Avalonia.Input.ScrollGestureEventArgs}) baseline/Avalonia/lib/net10.0/Avalonia.Base.dll current/Avalonia/lib/net10.0/Avalonia.Base.dll CP0002 - M:Avalonia.Media.RadialGradientBrush.set_Radius(System.Double) + M:Avalonia.Input.InputElement.AddScrollGestureInertiaStartingHandler(Avalonia.Interactivity.Interactive,System.EventHandler{Avalonia.Input.ScrollGestureInertiaStartingEventArgs}) baseline/Avalonia/lib/net10.0/Avalonia.Base.dll current/Avalonia/lib/net10.0/Avalonia.Base.dll CP0002 - M:Avalonia.Media.StreamGeometryContext.ArcTo(Avalonia.Point,Avalonia.Size,System.Double,System.Boolean,Avalonia.Media.SweepDirection) + M:Avalonia.Input.InputElement.OnGotFocus(Avalonia.Input.GotFocusEventArgs) baseline/Avalonia/lib/net10.0/Avalonia.Base.dll current/Avalonia/lib/net10.0/Avalonia.Base.dll CP0002 - M:Avalonia.Media.StreamGeometryContext.CubicBezierTo(Avalonia.Point,Avalonia.Point,Avalonia.Point) + M:Avalonia.Input.InputElement.OnLostFocus(Avalonia.Interactivity.RoutedEventArgs) baseline/Avalonia/lib/net10.0/Avalonia.Base.dll current/Avalonia/lib/net10.0/Avalonia.Base.dll CP0002 - M:Avalonia.Media.StreamGeometryContext.LineTo(Avalonia.Point) + M:Avalonia.Input.InputElement.RemovePinchEndedHandler(Avalonia.Interactivity.Interactive,System.EventHandler{Avalonia.Input.PinchEndedEventArgs}) baseline/Avalonia/lib/net10.0/Avalonia.Base.dll current/Avalonia/lib/net10.0/Avalonia.Base.dll CP0002 - M:Avalonia.Media.StreamGeometryContext.QuadraticBezierTo(Avalonia.Point,Avalonia.Point) + M:Avalonia.Input.InputElement.RemovePinchHandler(Avalonia.Interactivity.Interactive,System.EventHandler{Avalonia.Input.PinchEventArgs}) baseline/Avalonia/lib/net10.0/Avalonia.Base.dll current/Avalonia/lib/net10.0/Avalonia.Base.dll CP0002 - M:Avalonia.Media.TextFormatting.GenericTextRunProperties.#ctor(Avalonia.Media.Typeface,Avalonia.Media.FontFeatureCollection,System.Double,Avalonia.Media.TextDecorationCollection,Avalonia.Media.IBrush,Avalonia.Media.IBrush,Avalonia.Media.BaselineAlignment,System.Globalization.CultureInfo) + M:Avalonia.Input.InputElement.RemovePointerTouchPadGestureMagnifyHandler(Avalonia.Interactivity.Interactive,System.EventHandler{Avalonia.Input.PointerDeltaEventArgs}) baseline/Avalonia/lib/net10.0/Avalonia.Base.dll current/Avalonia/lib/net10.0/Avalonia.Base.dll CP0002 - M:Avalonia.Media.TextFormatting.GenericTextRunProperties.#ctor(Avalonia.Media.Typeface,System.Double,Avalonia.Media.TextDecorationCollection,Avalonia.Media.IBrush,Avalonia.Media.IBrush,Avalonia.Media.BaselineAlignment,System.Globalization.CultureInfo) + M:Avalonia.Input.InputElement.RemovePointerTouchPadGestureRotateHandler(Avalonia.Interactivity.Interactive,System.EventHandler{Avalonia.Input.PointerDeltaEventArgs}) baseline/Avalonia/lib/net10.0/Avalonia.Base.dll current/Avalonia/lib/net10.0/Avalonia.Base.dll CP0002 - M:Avalonia.Media.TextFormatting.TextCollapsingProperties.CreateCollapsedRuns(Avalonia.Media.TextFormatting.TextLine,System.Int32,Avalonia.Media.FlowDirection,Avalonia.Media.TextFormatting.TextRun) + M:Avalonia.Input.InputElement.RemovePointerTouchPadGestureSwipeHandler(Avalonia.Interactivity.Interactive,System.EventHandler{Avalonia.Input.PointerDeltaEventArgs}) baseline/Avalonia/lib/net10.0/Avalonia.Base.dll current/Avalonia/lib/net10.0/Avalonia.Base.dll CP0002 - M:Avalonia.Media.TextFormatting.TextLayout.#ctor(System.String,Avalonia.Media.Typeface,Avalonia.Media.FontFeatureCollection,System.Double,Avalonia.Media.IBrush,Avalonia.Media.TextAlignment,Avalonia.Media.TextWrapping,Avalonia.Media.TextTrimming,Avalonia.Media.TextDecorationCollection,Avalonia.Media.FlowDirection,System.Double,System.Double,System.Double,System.Double,System.Int32,System.Collections.Generic.IReadOnlyList{Avalonia.Utilities.ValueSpan{Avalonia.Media.TextFormatting.TextRunProperties}}) + M:Avalonia.Input.InputElement.RemovePullGestureEndedHandler(Avalonia.Interactivity.Interactive,System.EventHandler{Avalonia.Input.PullGestureEndedEventArgs}) baseline/Avalonia/lib/net10.0/Avalonia.Base.dll current/Avalonia/lib/net10.0/Avalonia.Base.dll CP0002 - M:Avalonia.Media.TextFormatting.TextLayout.#ctor(System.String,Avalonia.Media.Typeface,System.Double,Avalonia.Media.IBrush,Avalonia.Media.TextAlignment,Avalonia.Media.TextWrapping,Avalonia.Media.TextTrimming,Avalonia.Media.TextDecorationCollection,Avalonia.Media.FlowDirection,System.Double,System.Double,System.Double,System.Double,System.Int32,System.Collections.Generic.IReadOnlyList{Avalonia.Utilities.ValueSpan{Avalonia.Media.TextFormatting.TextRunProperties}}) + M:Avalonia.Input.InputElement.RemovePullGestureHandler(Avalonia.Interactivity.Interactive,System.EventHandler{Avalonia.Input.PullGestureEventArgs}) baseline/Avalonia/lib/net10.0/Avalonia.Base.dll current/Avalonia/lib/net10.0/Avalonia.Base.dll CP0002 - M:Avalonia.Media.TextFormatting.TextShaperOptions.#ctor(Avalonia.Media.GlyphTypeface,System.Collections.Generic.IReadOnlyList{Avalonia.Media.FontFeature},System.Double,System.SByte,System.Globalization.CultureInfo,System.Double,System.Double) + M:Avalonia.Input.InputElement.RemoveScrollGestureEndedHandler(Avalonia.Interactivity.Interactive,System.EventHandler{Avalonia.Input.ScrollGestureEndedEventArgs}) baseline/Avalonia/lib/net10.0/Avalonia.Base.dll current/Avalonia/lib/net10.0/Avalonia.Base.dll CP0002 - M:Avalonia.Media.TextFormatting.TextShaperOptions.#ctor(Avalonia.Media.GlyphTypeface,System.Double,System.SByte,System.Globalization.CultureInfo,System.Double,System.Double) + M:Avalonia.Input.InputElement.RemoveScrollGestureHandler(Avalonia.Interactivity.Interactive,System.EventHandler{Avalonia.Input.ScrollGestureEventArgs}) baseline/Avalonia/lib/net10.0/Avalonia.Base.dll current/Avalonia/lib/net10.0/Avalonia.Base.dll CP0002 - M:Avalonia.Platform.IDrawingContextImplWithEffects.PushEffect(Avalonia.Media.IEffect) + M:Avalonia.Input.InputElement.RemoveScrollGestureInertiaStartingHandler(Avalonia.Interactivity.Interactive,System.EventHandler{Avalonia.Input.ScrollGestureInertiaStartingEventArgs}) baseline/Avalonia/lib/net10.0/Avalonia.Base.dll current/Avalonia/lib/net10.0/Avalonia.Base.dll CP0002 - M:Avalonia.Platform.IFontManagerImpl.TryMatchCharacter(System.Int32,Avalonia.Media.FontStyle,Avalonia.Media.FontWeight,Avalonia.Media.FontStretch,System.Globalization.CultureInfo,Avalonia.Media.Typeface@) + M:Avalonia.Input.KeyboardNavigationHandler.Move(Avalonia.Input.IInputElement,Avalonia.Input.NavigationDirection,Avalonia.Input.KeyModifiers) baseline/Avalonia/lib/net10.0/Avalonia.Base.dll current/Avalonia/lib/net10.0/Avalonia.Base.dll CP0002 - M:Avalonia.Platform.IGeometryContext.ArcTo(Avalonia.Point,Avalonia.Size,System.Double,System.Boolean,Avalonia.Media.SweepDirection) + M:Avalonia.Input.Platform.IClipboard.GetDataAsync(System.String) baseline/Avalonia/lib/net10.0/Avalonia.Base.dll current/Avalonia/lib/net10.0/Avalonia.Base.dll CP0002 - M:Avalonia.Platform.IGeometryContext.CubicBezierTo(Avalonia.Point,Avalonia.Point,Avalonia.Point) + M:Avalonia.Input.Platform.IClipboard.GetFormatsAsync baseline/Avalonia/lib/net10.0/Avalonia.Base.dll current/Avalonia/lib/net10.0/Avalonia.Base.dll CP0002 - M:Avalonia.Platform.IGeometryContext.LineTo(Avalonia.Point) + M:Avalonia.Input.Platform.IClipboard.GetTextAsync baseline/Avalonia/lib/net10.0/Avalonia.Base.dll current/Avalonia/lib/net10.0/Avalonia.Base.dll CP0002 - M:Avalonia.Platform.IGeometryContext.QuadraticBezierTo(Avalonia.Point,Avalonia.Point) + M:Avalonia.Input.Platform.IClipboard.SetDataObjectAsync(Avalonia.Input.IDataObject) baseline/Avalonia/lib/net10.0/Avalonia.Base.dll current/Avalonia/lib/net10.0/Avalonia.Base.dll CP0002 - M:Avalonia.Platform.IPlatformRenderInterfaceContext.CreateOffscreenRenderTarget(Avalonia.PixelSize,System.Double) + M:Avalonia.Input.Platform.IClipboard.SetTextAsync(System.String) baseline/Avalonia/lib/net10.0/Avalonia.Base.dll current/Avalonia/lib/net10.0/Avalonia.Base.dll CP0002 - M:Avalonia.Platform.LockedFramebuffer.#ctor(System.IntPtr,Avalonia.PixelSize,System.Int32,Avalonia.Vector,Avalonia.Platform.PixelFormat,System.Action) + M:Avalonia.Input.Platform.IClipboard.TryGetInProcessDataObjectAsync baseline/Avalonia/lib/net10.0/Avalonia.Base.dll current/Avalonia/lib/net10.0/Avalonia.Base.dll CP0002 - M:Avalonia.Rendering.Composition.ICompositionGpuImportedObject.get_ImportCompeted + M:Avalonia.Input.Platform.IPlatformDragSource.DoDragDrop(Avalonia.Input.PointerEventArgs,Avalonia.Input.IDataObject,Avalonia.Input.DragDropEffects) baseline/Avalonia/lib/net10.0/Avalonia.Base.dll current/Avalonia/lib/net10.0/Avalonia.Base.dll CP0002 - M:Avalonia.Utilities.AvaloniaResourcesIndexReaderWriter.WriteResources(System.IO.Stream,System.Collections.Generic.List{System.ValueTuple{System.String,System.Int32,System.Func{System.IO.Stream}}}) + M:Avalonia.Input.Raw.RawDragEvent.#ctor(Avalonia.Input.Raw.IDragDropDevice,Avalonia.Input.Raw.RawDragEventType,Avalonia.Input.IInputRoot,Avalonia.Point,Avalonia.Input.IDataObject,Avalonia.Input.DragDropEffects,Avalonia.Input.RawInputModifiers) baseline/Avalonia/lib/net10.0/Avalonia.Base.dll current/Avalonia/lib/net10.0/Avalonia.Base.dll CP0002 - M:Avalonia.Visuals.Platform.PathGeometryContext.ArcTo(Avalonia.Point,Avalonia.Size,System.Double,System.Boolean,Avalonia.Media.SweepDirection) + M:Avalonia.Input.Raw.RawDragEvent.get_Data baseline/Avalonia/lib/net10.0/Avalonia.Base.dll current/Avalonia/lib/net10.0/Avalonia.Base.dll CP0002 - M:Avalonia.Visuals.Platform.PathGeometryContext.CubicBezierTo(Avalonia.Point,Avalonia.Point,Avalonia.Point) + M:Avalonia.Input.Raw.RawKeyEventArgs.#ctor(Avalonia.Input.IInputDevice,System.UInt64,Avalonia.Input.IInputRoot,Avalonia.Input.Raw.RawKeyEventType,Avalonia.Input.Key,Avalonia.Input.RawInputModifiers,Avalonia.Input.PhysicalKey,Avalonia.Input.KeyDeviceType,System.String) baseline/Avalonia/lib/net10.0/Avalonia.Base.dll current/Avalonia/lib/net10.0/Avalonia.Base.dll CP0002 - M:Avalonia.Visuals.Platform.PathGeometryContext.LineTo(Avalonia.Point) + M:Avalonia.Input.Raw.RawKeyEventArgs.#ctor(Avalonia.Input.IInputDevice,System.UInt64,Avalonia.Input.IInputRoot,Avalonia.Input.Raw.RawKeyEventType,Avalonia.Input.Key,Avalonia.Input.RawInputModifiers,Avalonia.Input.PhysicalKey,System.String) baseline/Avalonia/lib/net10.0/Avalonia.Base.dll current/Avalonia/lib/net10.0/Avalonia.Base.dll CP0002 - M:Avalonia.Visuals.Platform.PathGeometryContext.QuadraticBezierTo(Avalonia.Point,Avalonia.Point) + M:Avalonia.Input.Raw.RawKeyEventArgs.#ctor(Avalonia.Input.IKeyboardDevice,System.UInt64,Avalonia.Input.IInputRoot,Avalonia.Input.Raw.RawKeyEventType,Avalonia.Input.Key,Avalonia.Input.RawInputModifiers) baseline/Avalonia/lib/net10.0/Avalonia.Base.dll current/Avalonia/lib/net10.0/Avalonia.Base.dll CP0002 - F:Avalonia.Controls.ContextMenu.PlacementModeProperty - baseline/Avalonia/lib/net10.0/Avalonia.Controls.dll - current/Avalonia/lib/net10.0/Avalonia.Controls.dll + M:Avalonia.Input.SwipeGestureEventArgs.#ctor(System.Int32,Avalonia.Input.SwipeDirection,Avalonia.Vector,Avalonia.Point) + baseline/Avalonia/lib/net10.0/Avalonia.Base.dll + current/Avalonia/lib/net10.0/Avalonia.Base.dll CP0002 - F:Avalonia.Controls.Documents.Inline.TextDecorationsProperty - baseline/Avalonia/lib/net10.0/Avalonia.Controls.dll - current/Avalonia/lib/net10.0/Avalonia.Controls.dll + M:Avalonia.Input.SwipeGestureEventArgs.get_StartPoint + baseline/Avalonia/lib/net10.0/Avalonia.Base.dll + current/Avalonia/lib/net10.0/Avalonia.Base.dll CP0002 - F:Avalonia.Controls.NativeMenuBar.EnableMenuItemClickForwardingProperty - baseline/Avalonia/lib/net10.0/Avalonia.Controls.dll - current/Avalonia/lib/net10.0/Avalonia.Controls.dll + M:Avalonia.Input.TextInput.TextInputMethodClient.ShowInputPanel + baseline/Avalonia/lib/net10.0/Avalonia.Base.dll + current/Avalonia/lib/net10.0/Avalonia.Base.dll CP0002 - F:Avalonia.Controls.NativeMenuItem.ToggleTypeProperty - baseline/Avalonia/lib/net10.0/Avalonia.Controls.dll - current/Avalonia/lib/net10.0/Avalonia.Controls.dll + M:Avalonia.Layout.LayoutHelper.RoundLayoutSizeUp(Avalonia.Size,System.Double,System.Double) + baseline/Avalonia/lib/net10.0/Avalonia.Base.dll + current/Avalonia/lib/net10.0/Avalonia.Base.dll CP0002 - F:Avalonia.Controls.Primitives.Popup.PlacementModeProperty - baseline/Avalonia/lib/net10.0/Avalonia.Controls.dll - current/Avalonia/lib/net10.0/Avalonia.Controls.dll + M:Avalonia.Layout.LayoutHelper.RoundLayoutThickness(Avalonia.Thickness,System.Double,System.Double) + baseline/Avalonia/lib/net10.0/Avalonia.Base.dll + current/Avalonia/lib/net10.0/Avalonia.Base.dll CP0002 - F:Avalonia.Controls.Primitives.ToggleButton.CheckedEvent - baseline/Avalonia/lib/net10.0/Avalonia.Controls.dll - current/Avalonia/lib/net10.0/Avalonia.Controls.dll + M:Avalonia.Media.Color.ToUint32 + baseline/Avalonia/lib/net10.0/Avalonia.Base.dll + current/Avalonia/lib/net10.0/Avalonia.Base.dll CP0002 - F:Avalonia.Controls.Primitives.ToggleButton.IndeterminateEvent - baseline/Avalonia/lib/net10.0/Avalonia.Controls.dll - current/Avalonia/lib/net10.0/Avalonia.Controls.dll + M:Avalonia.Media.DrawingContext.PushPostTransform(Avalonia.Matrix) + baseline/Avalonia/lib/net10.0/Avalonia.Base.dll + current/Avalonia/lib/net10.0/Avalonia.Base.dll CP0002 - F:Avalonia.Controls.Primitives.ToggleButton.UncheckedEvent - baseline/Avalonia/lib/net10.0/Avalonia.Controls.dll - current/Avalonia/lib/net10.0/Avalonia.Controls.dll + M:Avalonia.Media.DrawingContext.PushPreTransform(Avalonia.Matrix) + baseline/Avalonia/lib/net10.0/Avalonia.Base.dll + current/Avalonia/lib/net10.0/Avalonia.Base.dll CP0002 - F:Avalonia.Controls.TextBlock.LetterSpacingProperty - baseline/Avalonia/lib/net10.0/Avalonia.Controls.dll - current/Avalonia/lib/net10.0/Avalonia.Controls.dll - - + M:Avalonia.Media.DrawingContext.PushTransformContainer + baseline/Avalonia/lib/net10.0/Avalonia.Base.dll + current/Avalonia/lib/net10.0/Avalonia.Base.dll + + + CP0002 + M:Avalonia.Media.DrawingImage.get_Viewbox + baseline/Avalonia/lib/net10.0/Avalonia.Base.dll + current/Avalonia/lib/net10.0/Avalonia.Base.dll + + + CP0002 + M:Avalonia.Media.DrawingImage.set_Viewbox(System.Nullable{Avalonia.Rect}) + baseline/Avalonia/lib/net10.0/Avalonia.Base.dll + current/Avalonia/lib/net10.0/Avalonia.Base.dll + + + CP0002 + M:Avalonia.Media.Fonts.FontCollectionBase.Initialize(Avalonia.Platform.IFontManagerImpl) + baseline/Avalonia/lib/net10.0/Avalonia.Base.dll + current/Avalonia/lib/net10.0/Avalonia.Base.dll + + + CP0002 + M:Avalonia.Media.Fonts.FontCollectionBase.TryGetGlyphTypeface(System.String,Avalonia.Media.Fonts.FontCollectionKey,Avalonia.Media.GlyphTypeface@) + baseline/Avalonia/lib/net10.0/Avalonia.Base.dll + current/Avalonia/lib/net10.0/Avalonia.Base.dll + + + CP0002 + M:Avalonia.Media.Fonts.IFontCollection.Initialize(Avalonia.Platform.IFontManagerImpl) + baseline/Avalonia/lib/net10.0/Avalonia.Base.dll + current/Avalonia/lib/net10.0/Avalonia.Base.dll + + + CP0002 + M:Avalonia.Media.Imaging.Bitmap.CopyPixels(Avalonia.Platform.ILockedFramebuffer,Avalonia.Platform.AlphaFormat) + baseline/Avalonia/lib/net10.0/Avalonia.Base.dll + current/Avalonia/lib/net10.0/Avalonia.Base.dll + + + CP0002 + M:Avalonia.Media.Immutable.ImmutableRadialGradientBrush.get_Radius + baseline/Avalonia/lib/net10.0/Avalonia.Base.dll + current/Avalonia/lib/net10.0/Avalonia.Base.dll + + + CP0002 + M:Avalonia.Media.IRadialGradientBrush.get_Radius + baseline/Avalonia/lib/net10.0/Avalonia.Base.dll + current/Avalonia/lib/net10.0/Avalonia.Base.dll + + + CP0002 + M:Avalonia.Media.RadialGradientBrush.get_Radius + baseline/Avalonia/lib/net10.0/Avalonia.Base.dll + current/Avalonia/lib/net10.0/Avalonia.Base.dll + + + CP0002 + M:Avalonia.Media.RadialGradientBrush.set_Radius(System.Double) + baseline/Avalonia/lib/net10.0/Avalonia.Base.dll + current/Avalonia/lib/net10.0/Avalonia.Base.dll + + + CP0002 + M:Avalonia.Media.StreamGeometryContext.ArcTo(Avalonia.Point,Avalonia.Size,System.Double,System.Boolean,Avalonia.Media.SweepDirection) + baseline/Avalonia/lib/net10.0/Avalonia.Base.dll + current/Avalonia/lib/net10.0/Avalonia.Base.dll + + + CP0002 + M:Avalonia.Media.StreamGeometryContext.CubicBezierTo(Avalonia.Point,Avalonia.Point,Avalonia.Point) + baseline/Avalonia/lib/net10.0/Avalonia.Base.dll + current/Avalonia/lib/net10.0/Avalonia.Base.dll + + + CP0002 + M:Avalonia.Media.StreamGeometryContext.LineTo(Avalonia.Point) + baseline/Avalonia/lib/net10.0/Avalonia.Base.dll + current/Avalonia/lib/net10.0/Avalonia.Base.dll + + + CP0002 + M:Avalonia.Media.StreamGeometryContext.QuadraticBezierTo(Avalonia.Point,Avalonia.Point) + baseline/Avalonia/lib/net10.0/Avalonia.Base.dll + current/Avalonia/lib/net10.0/Avalonia.Base.dll + + + CP0002 + M:Avalonia.Media.TextFormatting.GenericTextRunProperties.#ctor(Avalonia.Media.Typeface,Avalonia.Media.FontFeatureCollection,System.Double,Avalonia.Media.TextDecorationCollection,Avalonia.Media.IBrush,Avalonia.Media.IBrush,Avalonia.Media.BaselineAlignment,System.Globalization.CultureInfo) + baseline/Avalonia/lib/net10.0/Avalonia.Base.dll + current/Avalonia/lib/net10.0/Avalonia.Base.dll + + + CP0002 + M:Avalonia.Media.TextFormatting.GenericTextRunProperties.#ctor(Avalonia.Media.Typeface,System.Double,Avalonia.Media.TextDecorationCollection,Avalonia.Media.IBrush,Avalonia.Media.IBrush,Avalonia.Media.BaselineAlignment,System.Globalization.CultureInfo) + baseline/Avalonia/lib/net10.0/Avalonia.Base.dll + current/Avalonia/lib/net10.0/Avalonia.Base.dll + + + CP0002 + M:Avalonia.Media.TextFormatting.TextCollapsingProperties.CreateCollapsedRuns(Avalonia.Media.TextFormatting.TextLine,System.Int32,Avalonia.Media.FlowDirection,Avalonia.Media.TextFormatting.TextRun) + baseline/Avalonia/lib/net10.0/Avalonia.Base.dll + current/Avalonia/lib/net10.0/Avalonia.Base.dll + + + CP0002 + M:Avalonia.Media.TextFormatting.TextLayout.#ctor(System.String,Avalonia.Media.Typeface,Avalonia.Media.FontFeatureCollection,System.Double,Avalonia.Media.IBrush,Avalonia.Media.TextAlignment,Avalonia.Media.TextWrapping,Avalonia.Media.TextTrimming,Avalonia.Media.TextDecorationCollection,Avalonia.Media.FlowDirection,System.Double,System.Double,System.Double,System.Double,System.Int32,System.Collections.Generic.IReadOnlyList{Avalonia.Utilities.ValueSpan{Avalonia.Media.TextFormatting.TextRunProperties}}) + baseline/Avalonia/lib/net10.0/Avalonia.Base.dll + current/Avalonia/lib/net10.0/Avalonia.Base.dll + + + CP0002 + M:Avalonia.Media.TextFormatting.TextLayout.#ctor(System.String,Avalonia.Media.Typeface,System.Double,Avalonia.Media.IBrush,Avalonia.Media.TextAlignment,Avalonia.Media.TextWrapping,Avalonia.Media.TextTrimming,Avalonia.Media.TextDecorationCollection,Avalonia.Media.FlowDirection,System.Double,System.Double,System.Double,System.Double,System.Int32,System.Collections.Generic.IReadOnlyList{Avalonia.Utilities.ValueSpan{Avalonia.Media.TextFormatting.TextRunProperties}}) + baseline/Avalonia/lib/net10.0/Avalonia.Base.dll + current/Avalonia/lib/net10.0/Avalonia.Base.dll + + + CP0002 + M:Avalonia.Media.TextFormatting.TextShaperOptions.#ctor(Avalonia.Media.GlyphTypeface,System.Collections.Generic.IReadOnlyList{Avalonia.Media.FontFeature},System.Double,System.SByte,System.Globalization.CultureInfo,System.Double,System.Double) + baseline/Avalonia/lib/net10.0/Avalonia.Base.dll + current/Avalonia/lib/net10.0/Avalonia.Base.dll + + + CP0002 + M:Avalonia.Media.TextFormatting.TextShaperOptions.#ctor(Avalonia.Media.GlyphTypeface,System.Double,System.SByte,System.Globalization.CultureInfo,System.Double,System.Double) + baseline/Avalonia/lib/net10.0/Avalonia.Base.dll + current/Avalonia/lib/net10.0/Avalonia.Base.dll + + + CP0002 + M:Avalonia.Platform.ICursorFactory.CreateCursor(Avalonia.Platform.IBitmapImpl,Avalonia.PixelPoint) + baseline/Avalonia/lib/net10.0/Avalonia.Base.dll + current/Avalonia/lib/net10.0/Avalonia.Base.dll + + + CP0002 + M:Avalonia.Platform.IDrawingContextImplWithEffects.PushEffect(Avalonia.Media.IEffect) + baseline/Avalonia/lib/net10.0/Avalonia.Base.dll + current/Avalonia/lib/net10.0/Avalonia.Base.dll + + + CP0002 + M:Avalonia.Platform.IFontManagerImpl.TryMatchCharacter(System.Int32,Avalonia.Media.FontStyle,Avalonia.Media.FontWeight,Avalonia.Media.FontStretch,System.Globalization.CultureInfo,Avalonia.Media.Typeface@) + baseline/Avalonia/lib/net10.0/Avalonia.Base.dll + current/Avalonia/lib/net10.0/Avalonia.Base.dll + + + CP0002 + M:Avalonia.Platform.IGeometryContext.ArcTo(Avalonia.Point,Avalonia.Size,System.Double,System.Boolean,Avalonia.Media.SweepDirection) + baseline/Avalonia/lib/net10.0/Avalonia.Base.dll + current/Avalonia/lib/net10.0/Avalonia.Base.dll + + + CP0002 + M:Avalonia.Platform.IGeometryContext.CubicBezierTo(Avalonia.Point,Avalonia.Point,Avalonia.Point) + baseline/Avalonia/lib/net10.0/Avalonia.Base.dll + current/Avalonia/lib/net10.0/Avalonia.Base.dll + + + CP0002 + M:Avalonia.Platform.IGeometryContext.LineTo(Avalonia.Point) + baseline/Avalonia/lib/net10.0/Avalonia.Base.dll + current/Avalonia/lib/net10.0/Avalonia.Base.dll + + + CP0002 + M:Avalonia.Platform.IGeometryContext.QuadraticBezierTo(Avalonia.Point,Avalonia.Point) + baseline/Avalonia/lib/net10.0/Avalonia.Base.dll + current/Avalonia/lib/net10.0/Avalonia.Base.dll + + + CP0002 + M:Avalonia.Platform.IPlatformRenderInterfaceContext.CreateOffscreenRenderTarget(Avalonia.PixelSize,System.Double) + baseline/Avalonia/lib/net10.0/Avalonia.Base.dll + current/Avalonia/lib/net10.0/Avalonia.Base.dll + + + CP0002 + M:Avalonia.Platform.IPlatformRenderInterfaceContext.CreateRenderTarget(System.Collections.Generic.IEnumerable{System.Object}) + baseline/Avalonia/lib/net10.0/Avalonia.Base.dll + current/Avalonia/lib/net10.0/Avalonia.Base.dll + + + CP0002 + M:Avalonia.Platform.IRenderTarget.CreateDrawingContext(Avalonia.PixelSize,Avalonia.Platform.RenderTargetDrawingContextProperties@) + baseline/Avalonia/lib/net10.0/Avalonia.Base.dll + current/Avalonia/lib/net10.0/Avalonia.Base.dll + + + CP0002 + M:Avalonia.Platform.IRenderTarget.CreateDrawingContext(System.Boolean) + baseline/Avalonia/lib/net10.0/Avalonia.Base.dll + current/Avalonia/lib/net10.0/Avalonia.Base.dll + + + CP0002 + M:Avalonia.Platform.LockedFramebuffer.#ctor(System.IntPtr,Avalonia.PixelSize,System.Int32,Avalonia.Vector,Avalonia.Platform.PixelFormat,System.Action) + baseline/Avalonia/lib/net10.0/Avalonia.Base.dll + current/Avalonia/lib/net10.0/Avalonia.Base.dll + + + CP0002 + M:Avalonia.Rendering.Composition.ICompositionGpuImportedObject.get_ImportCompeted + baseline/Avalonia/lib/net10.0/Avalonia.Base.dll + current/Avalonia/lib/net10.0/Avalonia.Base.dll + + + CP0002 + M:Avalonia.Rendering.DefaultRenderTimer.add_Tick(System.Action{System.TimeSpan}) + baseline/Avalonia/lib/net10.0/Avalonia.Base.dll + current/Avalonia/lib/net10.0/Avalonia.Base.dll + + + CP0002 + M:Avalonia.Rendering.DefaultRenderTimer.remove_Tick(System.Action{System.TimeSpan}) + baseline/Avalonia/lib/net10.0/Avalonia.Base.dll + current/Avalonia/lib/net10.0/Avalonia.Base.dll + + + CP0002 + M:Avalonia.Rendering.DefaultRenderTimer.Start + baseline/Avalonia/lib/net10.0/Avalonia.Base.dll + current/Avalonia/lib/net10.0/Avalonia.Base.dll + + + CP0002 + M:Avalonia.Rendering.DefaultRenderTimer.Stop + baseline/Avalonia/lib/net10.0/Avalonia.Base.dll + current/Avalonia/lib/net10.0/Avalonia.Base.dll + + + CP0002 + M:Avalonia.Rendering.IRenderTimer.add_Tick(System.Action{System.TimeSpan}) + baseline/Avalonia/lib/net10.0/Avalonia.Base.dll + current/Avalonia/lib/net10.0/Avalonia.Base.dll + + + CP0002 + M:Avalonia.Rendering.IRenderTimer.remove_Tick(System.Action{System.TimeSpan}) + baseline/Avalonia/lib/net10.0/Avalonia.Base.dll + current/Avalonia/lib/net10.0/Avalonia.Base.dll + + + CP0002 + M:Avalonia.Rendering.SceneInvalidatedEventArgs.#ctor(Avalonia.Rendering.IRenderRoot,Avalonia.Rect) + baseline/Avalonia/lib/net10.0/Avalonia.Base.dll + current/Avalonia/lib/net10.0/Avalonia.Base.dll + + + CP0002 + M:Avalonia.Rendering.SceneInvalidatedEventArgs.get_RenderRoot + baseline/Avalonia/lib/net10.0/Avalonia.Base.dll + current/Avalonia/lib/net10.0/Avalonia.Base.dll + + + CP0002 + M:Avalonia.Rendering.SleepLoopRenderTimer.add_Tick(System.Action{System.TimeSpan}) + baseline/Avalonia/lib/net10.0/Avalonia.Base.dll + current/Avalonia/lib/net10.0/Avalonia.Base.dll + + + CP0002 + M:Avalonia.Rendering.SleepLoopRenderTimer.remove_Tick(System.Action{System.TimeSpan}) + baseline/Avalonia/lib/net10.0/Avalonia.Base.dll + current/Avalonia/lib/net10.0/Avalonia.Base.dll + + + CP0002 + M:Avalonia.Rendering.ThreadProxyRenderTimer.add_Tick(System.Action{System.TimeSpan}) + baseline/Avalonia/lib/net10.0/Avalonia.Base.dll + current/Avalonia/lib/net10.0/Avalonia.Base.dll + + + CP0002 + M:Avalonia.Rendering.ThreadProxyRenderTimer.remove_Tick(System.Action{System.TimeSpan}) + baseline/Avalonia/lib/net10.0/Avalonia.Base.dll + current/Avalonia/lib/net10.0/Avalonia.Base.dll + + + CP0002 + M:Avalonia.Utilities.AvaloniaResourcesIndexReaderWriter.WriteResources(System.IO.Stream,System.Collections.Generic.List{System.ValueTuple{System.String,System.Int32,System.Func{System.IO.Stream}}}) + baseline/Avalonia/lib/net10.0/Avalonia.Base.dll + current/Avalonia/lib/net10.0/Avalonia.Base.dll + + + CP0002 + M:Avalonia.Visual.get_VisualRoot + baseline/Avalonia/lib/net10.0/Avalonia.Base.dll + current/Avalonia/lib/net10.0/Avalonia.Base.dll + + + CP0002 + M:Avalonia.Visuals.Platform.PathGeometryContext.ArcTo(Avalonia.Point,Avalonia.Size,System.Double,System.Boolean,Avalonia.Media.SweepDirection) + baseline/Avalonia/lib/net10.0/Avalonia.Base.dll + current/Avalonia/lib/net10.0/Avalonia.Base.dll + + + CP0002 + M:Avalonia.Visuals.Platform.PathGeometryContext.CubicBezierTo(Avalonia.Point,Avalonia.Point,Avalonia.Point) + baseline/Avalonia/lib/net10.0/Avalonia.Base.dll + current/Avalonia/lib/net10.0/Avalonia.Base.dll + + + CP0002 + M:Avalonia.Visuals.Platform.PathGeometryContext.LineTo(Avalonia.Point) + baseline/Avalonia/lib/net10.0/Avalonia.Base.dll + current/Avalonia/lib/net10.0/Avalonia.Base.dll + + + CP0002 + M:Avalonia.Visuals.Platform.PathGeometryContext.QuadraticBezierTo(Avalonia.Point,Avalonia.Point) + baseline/Avalonia/lib/net10.0/Avalonia.Base.dll + current/Avalonia/lib/net10.0/Avalonia.Base.dll + + + CP0002 + M:Avalonia.VisualTree.VisualExtensions.GetVisualRoot(Avalonia.Visual) + baseline/Avalonia/lib/net10.0/Avalonia.Base.dll + current/Avalonia/lib/net10.0/Avalonia.Base.dll + + + CP0002 + M:Avalonia.VisualTreeAttachmentEventArgs.#ctor(Avalonia.Visual,Avalonia.Rendering.IRenderRoot) + baseline/Avalonia/lib/net10.0/Avalonia.Base.dll + current/Avalonia/lib/net10.0/Avalonia.Base.dll + + + CP0002 + M:Avalonia.VisualTreeAttachmentEventArgs.get_Root + baseline/Avalonia/lib/net10.0/Avalonia.Base.dll + current/Avalonia/lib/net10.0/Avalonia.Base.dll + + + CP0002 + F:Avalonia.Controls.ContextMenu.PlacementModeProperty + baseline/Avalonia/lib/net10.0/Avalonia.Controls.dll + current/Avalonia/lib/net10.0/Avalonia.Controls.dll + + + CP0002 + F:Avalonia.Controls.Control.ContextRequestedEvent + baseline/Avalonia/lib/net10.0/Avalonia.Controls.dll + current/Avalonia/lib/net10.0/Avalonia.Controls.dll + + + CP0002 + F:Avalonia.Controls.Documents.Inline.TextDecorationsProperty + baseline/Avalonia/lib/net10.0/Avalonia.Controls.dll + current/Avalonia/lib/net10.0/Avalonia.Controls.dll + + + CP0002 + F:Avalonia.Controls.DrawerPage.DrawerBreakpointWidthProperty + baseline/Avalonia/lib/net10.0/Avalonia.Controls.dll + current/Avalonia/lib/net10.0/Avalonia.Controls.dll + + + CP0002 + F:Avalonia.Controls.NativeMenuBar.EnableMenuItemClickForwardingProperty + baseline/Avalonia/lib/net10.0/Avalonia.Controls.dll + current/Avalonia/lib/net10.0/Avalonia.Controls.dll + + + CP0002 + F:Avalonia.Controls.NativeMenuItem.ToggleTypeProperty + baseline/Avalonia/lib/net10.0/Avalonia.Controls.dll + current/Avalonia/lib/net10.0/Avalonia.Controls.dll + + + CP0002 + F:Avalonia.Controls.Primitives.FlyoutBase.IsOpenProperty + baseline/Avalonia/lib/net10.0/Avalonia.Controls.dll + current/Avalonia/lib/net10.0/Avalonia.Controls.dll + + + CP0002 + F:Avalonia.Controls.Primitives.Popup.PlacementModeProperty + baseline/Avalonia/lib/net10.0/Avalonia.Controls.dll + current/Avalonia/lib/net10.0/Avalonia.Controls.dll + + + CP0002 + F:Avalonia.Controls.Primitives.ToggleButton.CheckedEvent + baseline/Avalonia/lib/net10.0/Avalonia.Controls.dll + current/Avalonia/lib/net10.0/Avalonia.Controls.dll + + + CP0002 + F:Avalonia.Controls.Primitives.ToggleButton.IndeterminateEvent + baseline/Avalonia/lib/net10.0/Avalonia.Controls.dll + current/Avalonia/lib/net10.0/Avalonia.Controls.dll + + + CP0002 + F:Avalonia.Controls.Primitives.ToggleButton.UncheckedEvent + baseline/Avalonia/lib/net10.0/Avalonia.Controls.dll + current/Avalonia/lib/net10.0/Avalonia.Controls.dll + + + CP0002 + F:Avalonia.Controls.Primitives.VisualLayerManager.ChromeOverlayLayerProperty + baseline/Avalonia/lib/net10.0/Avalonia.Controls.dll + current/Avalonia/lib/net10.0/Avalonia.Controls.dll + + + CP0002 + F:Avalonia.Controls.TabItem.IconProperty + baseline/Avalonia/lib/net10.0/Avalonia.Controls.dll + current/Avalonia/lib/net10.0/Avalonia.Controls.dll + + + CP0002 + F:Avalonia.Controls.TextBlock.LetterSpacingProperty + baseline/Avalonia/lib/net10.0/Avalonia.Controls.dll + current/Avalonia/lib/net10.0/Avalonia.Controls.dll + + + CP0002 + F:Avalonia.Controls.TextBox.LetterSpacingProperty + baseline/Avalonia/lib/net10.0/Avalonia.Controls.dll + current/Avalonia/lib/net10.0/Avalonia.Controls.dll + + + CP0002 + F:Avalonia.Controls.TopLevel.PointerOverElementProperty + baseline/Avalonia/lib/net10.0/Avalonia.Controls.dll + current/Avalonia/lib/net10.0/Avalonia.Controls.dll + + + CP0002 + F:Avalonia.Controls.Window.ExtendClientAreaChromeHintsProperty + baseline/Avalonia/lib/net10.0/Avalonia.Controls.dll + current/Avalonia/lib/net10.0/Avalonia.Controls.dll + + + CP0002 + F:Avalonia.Controls.Window.SystemDecorationsProperty + baseline/Avalonia/lib/net10.0/Avalonia.Controls.dll + current/Avalonia/lib/net10.0/Avalonia.Controls.dll + + + CP0002 + M:Avalonia.AppBuilder.get_LifetimeOverride + baseline/Avalonia/lib/net10.0/Avalonia.Controls.dll + current/Avalonia/lib/net10.0/Avalonia.Controls.dll + + + CP0002 + M:Avalonia.Application.add_UrlsOpened(System.EventHandler{Avalonia.UrlOpenedEventArgs}) + baseline/Avalonia/lib/net10.0/Avalonia.Controls.dll + current/Avalonia/lib/net10.0/Avalonia.Controls.dll + + + CP0002 + M:Avalonia.Application.remove_UrlsOpened(System.EventHandler{Avalonia.UrlOpenedEventArgs}) + baseline/Avalonia/lib/net10.0/Avalonia.Controls.dll + current/Avalonia/lib/net10.0/Avalonia.Controls.dll + + + CP0002 + M:Avalonia.Automation.Peers.AutomationPeer.GetVisualRootCore + baseline/Avalonia/lib/net10.0/Avalonia.Controls.dll + current/Avalonia/lib/net10.0/Avalonia.Controls.dll + + + CP0002 + M:Avalonia.Controls.ContextMenu.get_PlacementMode + baseline/Avalonia/lib/net10.0/Avalonia.Controls.dll + current/Avalonia/lib/net10.0/Avalonia.Controls.dll + + + CP0002 + M:Avalonia.Controls.ContextMenu.set_PlacementMode(Avalonia.Controls.PlacementMode) + baseline/Avalonia/lib/net10.0/Avalonia.Controls.dll + current/Avalonia/lib/net10.0/Avalonia.Controls.dll + + + CP0002 + M:Avalonia.Controls.Design.CreatePreviewWithControl(System.Object) + baseline/Avalonia/lib/net10.0/Avalonia.Controls.dll + current/Avalonia/lib/net10.0/Avalonia.Controls.dll + + + CP0002 + M:Avalonia.Controls.Design.GetDataContext(Avalonia.Controls.Templates.IDataTemplate) + baseline/Avalonia/lib/net10.0/Avalonia.Controls.dll + current/Avalonia/lib/net10.0/Avalonia.Controls.dll + + + CP0002 + M:Avalonia.Controls.Design.GetPreviewWith(Avalonia.Controls.Templates.IDataTemplate) + baseline/Avalonia/lib/net10.0/Avalonia.Controls.dll + current/Avalonia/lib/net10.0/Avalonia.Controls.dll + + + CP0002 + M:Avalonia.Controls.Design.GetPreviewWith(Avalonia.Styling.IStyle) + baseline/Avalonia/lib/net10.0/Avalonia.Controls.dll + current/Avalonia/lib/net10.0/Avalonia.Controls.dll + + + CP0002 + M:Avalonia.Controls.Design.SetDataContext(Avalonia.Controls.Templates.IDataTemplate,System.Object) + baseline/Avalonia/lib/net10.0/Avalonia.Controls.dll + current/Avalonia/lib/net10.0/Avalonia.Controls.dll + + + CP0002 + M:Avalonia.Controls.Design.SetPreviewWith(Avalonia.AvaloniaObject,Avalonia.Controls.Control) + baseline/Avalonia/lib/net10.0/Avalonia.Controls.dll + current/Avalonia/lib/net10.0/Avalonia.Controls.dll + + + CP0002 + M:Avalonia.Controls.Design.SetPreviewWith(Avalonia.AvaloniaObject,Avalonia.Controls.ITemplate{Avalonia.Controls.Control}) + baseline/Avalonia/lib/net10.0/Avalonia.Controls.dll + current/Avalonia/lib/net10.0/Avalonia.Controls.dll + + + CP0002 + M:Avalonia.Controls.Design.SetPreviewWith(Avalonia.Controls.ResourceDictionary,Avalonia.Controls.ITemplate{Avalonia.Controls.Control}) + baseline/Avalonia/lib/net10.0/Avalonia.Controls.dll + current/Avalonia/lib/net10.0/Avalonia.Controls.dll + + + CP0002 + M:Avalonia.Controls.Design.SetPreviewWith(Avalonia.Controls.Templates.IDataTemplate,Avalonia.Controls.Control) + baseline/Avalonia/lib/net10.0/Avalonia.Controls.dll + current/Avalonia/lib/net10.0/Avalonia.Controls.dll + + + CP0002 + M:Avalonia.Controls.Design.SetPreviewWith(Avalonia.Controls.Templates.IDataTemplate,Avalonia.Controls.ITemplate{Avalonia.Controls.Control}) + baseline/Avalonia/lib/net10.0/Avalonia.Controls.dll + current/Avalonia/lib/net10.0/Avalonia.Controls.dll + + + CP0002 + M:Avalonia.Controls.Design.SetPreviewWith(Avalonia.Styling.IStyle,Avalonia.Controls.Control) + baseline/Avalonia/lib/net10.0/Avalonia.Controls.dll + current/Avalonia/lib/net10.0/Avalonia.Controls.dll + + + CP0002 + M:Avalonia.Controls.Design.SetPreviewWith(Avalonia.Styling.IStyle,Avalonia.Controls.ITemplate{Avalonia.Controls.Control}) + baseline/Avalonia/lib/net10.0/Avalonia.Controls.dll + current/Avalonia/lib/net10.0/Avalonia.Controls.dll + + + CP0002 + M:Avalonia.Controls.DrawerPage.get_DrawerBreakpointWidth + baseline/Avalonia/lib/net10.0/Avalonia.Controls.dll + current/Avalonia/lib/net10.0/Avalonia.Controls.dll + + + CP0002 + M:Avalonia.Controls.DrawerPage.set_DrawerBreakpointWidth(System.Double) + baseline/Avalonia/lib/net10.0/Avalonia.Controls.dll + current/Avalonia/lib/net10.0/Avalonia.Controls.dll + + + CP0002 + M:Avalonia.Controls.Embedding.Offscreen.OffscreenTopLevelImplBase.get_Surfaces + baseline/Avalonia/lib/net10.0/Avalonia.Controls.dll + current/Avalonia/lib/net10.0/Avalonia.Controls.dll + + + CP0002 + M:Avalonia.Controls.Generators.ItemContainerGenerator.ContainerFromIndex(System.Int32) + baseline/Avalonia/lib/net10.0/Avalonia.Controls.dll + current/Avalonia/lib/net10.0/Avalonia.Controls.dll + + + CP0002 + M:Avalonia.Controls.Generators.ItemContainerGenerator.IndexFromContainer(Avalonia.Controls.Control) + baseline/Avalonia/lib/net10.0/Avalonia.Controls.dll + current/Avalonia/lib/net10.0/Avalonia.Controls.dll + + + CP0002 + M:Avalonia.Controls.ItemsControl.ItemsControlFromItemContaner(Avalonia.Controls.Control) + baseline/Avalonia/lib/net10.0/Avalonia.Controls.dll + current/Avalonia/lib/net10.0/Avalonia.Controls.dll + + + CP0002 + M:Avalonia.Controls.NativeMenuBar.SetEnableMenuItemClickForwarding(Avalonia.Controls.MenuItem,System.Boolean) + baseline/Avalonia/lib/net10.0/Avalonia.Controls.dll + current/Avalonia/lib/net10.0/Avalonia.Controls.dll + + + CP0002 + M:Avalonia.Controls.NativeMenuItem.get_ToggleType + baseline/Avalonia/lib/net10.0/Avalonia.Controls.dll + current/Avalonia/lib/net10.0/Avalonia.Controls.dll + + + CP0002 + M:Avalonia.Controls.PageSelectionChangedEventArgs.#ctor(Avalonia.Controls.Page,Avalonia.Controls.Page) + baseline/Avalonia/lib/net10.0/Avalonia.Controls.dll + current/Avalonia/lib/net10.0/Avalonia.Controls.dll + + + CP0002 + M:Avalonia.Controls.Platform.DefaultMenuInteractionHandler.GotFocus(System.Object,Avalonia.Input.GotFocusEventArgs) + baseline/Avalonia/lib/net10.0/Avalonia.Controls.dll + current/Avalonia/lib/net10.0/Avalonia.Controls.dll + + + CP0002 + M:Avalonia.Controls.Platform.IInsetsManager.get_DisplayEdgeToEdge + baseline/Avalonia/lib/net10.0/Avalonia.Controls.dll + current/Avalonia/lib/net10.0/Avalonia.Controls.dll + + + CP0002 + M:Avalonia.Controls.Platform.IInsetsManager.set_DisplayEdgeToEdge(System.Boolean) + baseline/Avalonia/lib/net10.0/Avalonia.Controls.dll + current/Avalonia/lib/net10.0/Avalonia.Controls.dll + + + CP0002 + M:Avalonia.Controls.Platform.InsetsManagerBase.get_DisplayEdgeToEdge + baseline/Avalonia/lib/net10.0/Avalonia.Controls.dll + current/Avalonia/lib/net10.0/Avalonia.Controls.dll + + + CP0002 + M:Avalonia.Controls.Platform.InsetsManagerBase.set_DisplayEdgeToEdge(System.Boolean) + baseline/Avalonia/lib/net10.0/Avalonia.Controls.dll + current/Avalonia/lib/net10.0/Avalonia.Controls.dll + + + CP0002 + M:Avalonia.Controls.Primitives.AccessText.get_AccessKey + baseline/Avalonia/lib/net10.0/Avalonia.Controls.dll + current/Avalonia/lib/net10.0/Avalonia.Controls.dll + + + CP0002 + M:Avalonia.Controls.Primitives.AdornerLayer.#ctor + baseline/Avalonia/lib/net10.0/Avalonia.Controls.dll + current/Avalonia/lib/net10.0/Avalonia.Controls.dll + + + CP0002 + M:Avalonia.Controls.Primitives.OverlayPopupHost.#ctor(Avalonia.Controls.Primitives.OverlayLayer) + baseline/Avalonia/lib/net10.0/Avalonia.Controls.dll + current/Avalonia/lib/net10.0/Avalonia.Controls.dll + + + CP0002 + M:Avalonia.Controls.Primitives.OverlayPopupHost.ConfigurePosition(Avalonia.Visual,Avalonia.Controls.PlacementMode,Avalonia.Point,Avalonia.Controls.Primitives.PopupPositioning.PopupAnchor,Avalonia.Controls.Primitives.PopupPositioning.PopupGravity,Avalonia.Controls.Primitives.PopupPositioning.PopupPositionerConstraintAdjustment,System.Nullable{Avalonia.Rect}) + baseline/Avalonia/lib/net10.0/Avalonia.Controls.dll + current/Avalonia/lib/net10.0/Avalonia.Controls.dll + + + CP0002 + M:Avalonia.Controls.Primitives.OverlayPopupHost.CreatePopupHost(Avalonia.Visual,Avalonia.IAvaloniaDependencyResolver,System.Boolean) + baseline/Avalonia/lib/net10.0/Avalonia.Controls.dll + current/Avalonia/lib/net10.0/Avalonia.Controls.dll + + + CP0002 + M:Avalonia.Controls.Primitives.OverlayPopupHost.CreatePopupHost(Avalonia.Visual,Avalonia.IAvaloniaDependencyResolver) + baseline/Avalonia/lib/net10.0/Avalonia.Controls.dll + current/Avalonia/lib/net10.0/Avalonia.Controls.dll + + + CP0002 + M:Avalonia.Controls.Primitives.OverlayPopupHost.SetChild(Avalonia.Controls.Control) + baseline/Avalonia/lib/net10.0/Avalonia.Controls.dll + current/Avalonia/lib/net10.0/Avalonia.Controls.dll + + + CP0002 + M:Avalonia.Controls.Primitives.OverlayPopupHost.TakeFocus + baseline/Avalonia/lib/net10.0/Avalonia.Controls.dll + current/Avalonia/lib/net10.0/Avalonia.Controls.dll + + + CP0002 + M:Avalonia.Controls.Primitives.Popup.get_Host + baseline/Avalonia/lib/net10.0/Avalonia.Controls.dll + current/Avalonia/lib/net10.0/Avalonia.Controls.dll + + + CP0002 + M:Avalonia.Controls.Primitives.Popup.get_PlacementMode + baseline/Avalonia/lib/net10.0/Avalonia.Controls.dll + current/Avalonia/lib/net10.0/Avalonia.Controls.dll + + + CP0002 + M:Avalonia.Controls.Primitives.Popup.set_PlacementMode(Avalonia.Controls.PlacementMode) + baseline/Avalonia/lib/net10.0/Avalonia.Controls.dll + current/Avalonia/lib/net10.0/Avalonia.Controls.dll + + + CP0002 + M:Avalonia.Controls.Primitives.PopupRoot.ConfigurePosition(Avalonia.Visual,Avalonia.Controls.PlacementMode,Avalonia.Point,Avalonia.Controls.Primitives.PopupPositioning.PopupAnchor,Avalonia.Controls.Primitives.PopupPositioning.PopupGravity,Avalonia.Controls.Primitives.PopupPositioning.PopupPositionerConstraintAdjustment,System.Nullable{Avalonia.Rect}) + baseline/Avalonia/lib/net10.0/Avalonia.Controls.dll + current/Avalonia/lib/net10.0/Avalonia.Controls.dll + + + CP0002 + M:Avalonia.Controls.Primitives.TextSearch.GetText(Avalonia.Controls.Control) + baseline/Avalonia/lib/net10.0/Avalonia.Controls.dll + current/Avalonia/lib/net10.0/Avalonia.Controls.dll + + + CP0002 + M:Avalonia.Controls.Primitives.TextSearch.GetText(Avalonia.Interactivity.Interactive) + baseline/Avalonia/lib/net10.0/Avalonia.Controls.dll + current/Avalonia/lib/net10.0/Avalonia.Controls.dll + + + CP0002 + M:Avalonia.Controls.Primitives.TextSearch.GetTextBinding(Avalonia.Interactivity.Interactive) + baseline/Avalonia/lib/net10.0/Avalonia.Controls.dll + current/Avalonia/lib/net10.0/Avalonia.Controls.dll + + + CP0002 + M:Avalonia.Controls.Primitives.TextSearch.SetText(Avalonia.Controls.Control,System.String) + baseline/Avalonia/lib/net10.0/Avalonia.Controls.dll + current/Avalonia/lib/net10.0/Avalonia.Controls.dll + + + CP0002 + M:Avalonia.Controls.Primitives.TextSearch.SetText(Avalonia.Interactivity.Interactive,System.String) + baseline/Avalonia/lib/net10.0/Avalonia.Controls.dll + current/Avalonia/lib/net10.0/Avalonia.Controls.dll + + + CP0002 + M:Avalonia.Controls.Primitives.TextSearch.SetTextBinding(Avalonia.Interactivity.Interactive,Avalonia.Data.BindingBase) + baseline/Avalonia/lib/net10.0/Avalonia.Controls.dll + current/Avalonia/lib/net10.0/Avalonia.Controls.dll + + + CP0002 + M:Avalonia.Controls.Primitives.ToggleButton.add_Checked(System.EventHandler{Avalonia.Interactivity.RoutedEventArgs}) + baseline/Avalonia/lib/net10.0/Avalonia.Controls.dll + current/Avalonia/lib/net10.0/Avalonia.Controls.dll + + + CP0002 + M:Avalonia.Controls.Primitives.ToggleButton.add_Indeterminate(System.EventHandler{Avalonia.Interactivity.RoutedEventArgs}) + baseline/Avalonia/lib/net10.0/Avalonia.Controls.dll + current/Avalonia/lib/net10.0/Avalonia.Controls.dll + + + CP0002 + M:Avalonia.Controls.Primitives.ToggleButton.add_Unchecked(System.EventHandler{Avalonia.Interactivity.RoutedEventArgs}) + baseline/Avalonia/lib/net10.0/Avalonia.Controls.dll + current/Avalonia/lib/net10.0/Avalonia.Controls.dll + + + CP0002 + M:Avalonia.Controls.Primitives.ToggleButton.OnChecked(Avalonia.Interactivity.RoutedEventArgs) + baseline/Avalonia/lib/net10.0/Avalonia.Controls.dll + current/Avalonia/lib/net10.0/Avalonia.Controls.dll + + + CP0002 + M:Avalonia.Controls.Primitives.ToggleButton.OnIndeterminate(Avalonia.Interactivity.RoutedEventArgs) + baseline/Avalonia/lib/net10.0/Avalonia.Controls.dll + current/Avalonia/lib/net10.0/Avalonia.Controls.dll + + + CP0002 + M:Avalonia.Controls.Primitives.ToggleButton.OnUnchecked(Avalonia.Interactivity.RoutedEventArgs) + baseline/Avalonia/lib/net10.0/Avalonia.Controls.dll + current/Avalonia/lib/net10.0/Avalonia.Controls.dll + + + CP0002 + M:Avalonia.Controls.Primitives.ToggleButton.remove_Checked(System.EventHandler{Avalonia.Interactivity.RoutedEventArgs}) + baseline/Avalonia/lib/net10.0/Avalonia.Controls.dll + current/Avalonia/lib/net10.0/Avalonia.Controls.dll + + + CP0002 + M:Avalonia.Controls.Primitives.ToggleButton.remove_Indeterminate(System.EventHandler{Avalonia.Interactivity.RoutedEventArgs}) + baseline/Avalonia/lib/net10.0/Avalonia.Controls.dll + current/Avalonia/lib/net10.0/Avalonia.Controls.dll + + + CP0002 + M:Avalonia.Controls.Primitives.ToggleButton.remove_Unchecked(System.EventHandler{Avalonia.Interactivity.RoutedEventArgs}) + baseline/Avalonia/lib/net10.0/Avalonia.Controls.dll + current/Avalonia/lib/net10.0/Avalonia.Controls.dll + + + CP0002 + M:Avalonia.Controls.Primitives.VisualLayerManager.get_AdornerLayer + baseline/Avalonia/lib/net10.0/Avalonia.Controls.dll + current/Avalonia/lib/net10.0/Avalonia.Controls.dll + + + CP0002 + M:Avalonia.Controls.Primitives.VisualLayerManager.get_ChromeOverlayLayer + baseline/Avalonia/lib/net10.0/Avalonia.Controls.dll + current/Avalonia/lib/net10.0/Avalonia.Controls.dll + + + CP0002 + M:Avalonia.Controls.Primitives.VisualLayerManager.get_IsPopup + baseline/Avalonia/lib/net10.0/Avalonia.Controls.dll + current/Avalonia/lib/net10.0/Avalonia.Controls.dll + + + CP0002 + M:Avalonia.Controls.Primitives.VisualLayerManager.get_LightDismissOverlayLayer + baseline/Avalonia/lib/net10.0/Avalonia.Controls.dll + current/Avalonia/lib/net10.0/Avalonia.Controls.dll + + + CP0002 + M:Avalonia.Controls.Primitives.VisualLayerManager.get_OverlayLayer + baseline/Avalonia/lib/net10.0/Avalonia.Controls.dll + current/Avalonia/lib/net10.0/Avalonia.Controls.dll + + + CP0002 + M:Avalonia.Controls.Primitives.VisualLayerManager.get_TextSelectorLayer + baseline/Avalonia/lib/net10.0/Avalonia.Controls.dll + current/Avalonia/lib/net10.0/Avalonia.Controls.dll + + + CP0002 + M:Avalonia.Controls.Primitives.VisualLayerManager.set_IsPopup(System.Boolean) + baseline/Avalonia/lib/net10.0/Avalonia.Controls.dll + current/Avalonia/lib/net10.0/Avalonia.Controls.dll + + + CP0002 + M:Avalonia.Controls.Screens.ScreenFromWindow(Avalonia.Platform.IWindowBaseImpl) + baseline/Avalonia/lib/net10.0/Avalonia.Controls.dll + current/Avalonia/lib/net10.0/Avalonia.Controls.dll + + + CP0002 + M:Avalonia.Controls.TabbedPage.FindNextEnabledTab(System.Int32,System.Int32) + baseline/Avalonia/lib/net10.0/Avalonia.Controls.dll + current/Avalonia/lib/net10.0/Avalonia.Controls.dll + + + CP0002 + M:Avalonia.Controls.TabItem.get_Icon + baseline/Avalonia/lib/net10.0/Avalonia.Controls.dll + current/Avalonia/lib/net10.0/Avalonia.Controls.dll + + + CP0002 + M:Avalonia.Controls.TabItem.SubscribeToOwnerProperties(Avalonia.AvaloniaObject) + baseline/Avalonia/lib/net10.0/Avalonia.Controls.dll + current/Avalonia/lib/net10.0/Avalonia.Controls.dll + + + CP0002 + M:Avalonia.Controls.TopLevel.#ctor(Avalonia.Platform.ITopLevelImpl,Avalonia.IAvaloniaDependencyResolver) + baseline/Avalonia/lib/net10.0/Avalonia.Controls.dll + current/Avalonia/lib/net10.0/Avalonia.Controls.dll + + + CP0002 + M:Avalonia.Controls.TopLevel.get_PlatformSettings + baseline/Avalonia/lib/net10.0/Avalonia.Controls.dll + current/Avalonia/lib/net10.0/Avalonia.Controls.dll + + + CP0002 + M:Avalonia.Controls.TopLevel.StartRendering + baseline/Avalonia/lib/net10.0/Avalonia.Controls.dll + current/Avalonia/lib/net10.0/Avalonia.Controls.dll + + + CP0002 + M:Avalonia.Controls.TopLevel.StopRendering + baseline/Avalonia/lib/net10.0/Avalonia.Controls.dll + current/Avalonia/lib/net10.0/Avalonia.Controls.dll + + + CP0002 + M:Avalonia.Controls.TreeView.get_ItemContainerGenerator + baseline/Avalonia/lib/net10.0/Avalonia.Controls.dll + current/Avalonia/lib/net10.0/Avalonia.Controls.dll + + + CP0002 + M:Avalonia.Controls.Window.get_ExtendClientAreaChromeHints + baseline/Avalonia/lib/net10.0/Avalonia.Controls.dll + current/Avalonia/lib/net10.0/Avalonia.Controls.dll + + + CP0002 + M:Avalonia.Controls.Window.get_SystemDecorations + baseline/Avalonia/lib/net10.0/Avalonia.Controls.dll + current/Avalonia/lib/net10.0/Avalonia.Controls.dll + + + CP0002 + M:Avalonia.Controls.Window.set_ExtendClientAreaChromeHints(Avalonia.Platform.ExtendClientAreaChromeHints) + baseline/Avalonia/lib/net10.0/Avalonia.Controls.dll + current/Avalonia/lib/net10.0/Avalonia.Controls.dll + + + CP0002 + M:Avalonia.Controls.Window.SortWindowsByZOrder(Avalonia.Controls.Window[]) + baseline/Avalonia/lib/net10.0/Avalonia.Controls.dll + current/Avalonia/lib/net10.0/Avalonia.Controls.dll + + + CP0002 + M:Avalonia.Controls.WindowBase.ArrangeSetBounds(Avalonia.Size) + baseline/Avalonia/lib/net10.0/Avalonia.Controls.dll + current/Avalonia/lib/net10.0/Avalonia.Controls.dll + + + CP0002 + M:Avalonia.Platform.ITopLevelImpl.get_Surfaces + baseline/Avalonia/lib/net10.0/Avalonia.Controls.dll + current/Avalonia/lib/net10.0/Avalonia.Controls.dll + + + CP0002 + M:Avalonia.Platform.IWindowImpl.GetWindowsZOrder(System.Span{Avalonia.Controls.Window},System.Span{System.Int64}) + baseline/Avalonia/lib/net10.0/Avalonia.Controls.dll + current/Avalonia/lib/net10.0/Avalonia.Controls.dll + + + CP0002 + M:Avalonia.Platform.IWindowImpl.SetExtendClientAreaChromeHints(Avalonia.Platform.ExtendClientAreaChromeHints) + baseline/Avalonia/lib/net10.0/Avalonia.Controls.dll + current/Avalonia/lib/net10.0/Avalonia.Controls.dll + + + CP0002 + M:Avalonia.Platform.IWindowImpl.SetSystemDecorations(Avalonia.Controls.SystemDecorations) + baseline/Avalonia/lib/net10.0/Avalonia.Controls.dll + current/Avalonia/lib/net10.0/Avalonia.Controls.dll + + + CP0002 + M:Avalonia.Platform.Screen.#ctor(System.Double,Avalonia.PixelRect,Avalonia.PixelRect,System.Boolean) + baseline/Avalonia/lib/net10.0/Avalonia.Controls.dll + current/Avalonia/lib/net10.0/Avalonia.Controls.dll + + + CP0002 + M:Avalonia.Platform.Screen.get_PixelDensity + baseline/Avalonia/lib/net10.0/Avalonia.Controls.dll + current/Avalonia/lib/net10.0/Avalonia.Controls.dll + + + CP0002 + M:Avalonia.Platform.Screen.get_Primary + baseline/Avalonia/lib/net10.0/Avalonia.Controls.dll + current/Avalonia/lib/net10.0/Avalonia.Controls.dll + + + CP0002 + M:Avalonia.Platform.Screen.set_Bounds(Avalonia.PixelRect) + baseline/Avalonia/lib/net10.0/Avalonia.Controls.dll + current/Avalonia/lib/net10.0/Avalonia.Controls.dll + + + CP0002 + M:Avalonia.Platform.Screen.set_CurrentOrientation(Avalonia.Platform.ScreenOrientation) + baseline/Avalonia/lib/net10.0/Avalonia.Controls.dll + current/Avalonia/lib/net10.0/Avalonia.Controls.dll + + + CP0002 + M:Avalonia.Platform.Screen.set_DisplayName(System.String) + baseline/Avalonia/lib/net10.0/Avalonia.Controls.dll + current/Avalonia/lib/net10.0/Avalonia.Controls.dll + + + CP0002 + M:Avalonia.Platform.Screen.set_IsPrimary(System.Boolean) + baseline/Avalonia/lib/net10.0/Avalonia.Controls.dll + current/Avalonia/lib/net10.0/Avalonia.Controls.dll + + + CP0002 + M:Avalonia.Platform.Screen.set_Scaling(System.Double) + baseline/Avalonia/lib/net10.0/Avalonia.Controls.dll + current/Avalonia/lib/net10.0/Avalonia.Controls.dll + + + CP0002 + M:Avalonia.Platform.Screen.set_WorkingArea(Avalonia.PixelRect) + baseline/Avalonia/lib/net10.0/Avalonia.Controls.dll + current/Avalonia/lib/net10.0/Avalonia.Controls.dll + + + CP0002 + M:Avalonia.Dialogs.ManagedFileDialogExtensions.ShowManagedAsync(Avalonia.Controls.OpenFileDialog,Avalonia.Controls.Window,Avalonia.Dialogs.ManagedFileDialogOptions) + baseline/Avalonia/lib/net10.0/Avalonia.Dialogs.dll + current/Avalonia/lib/net10.0/Avalonia.Dialogs.dll + + + CP0002 + M:Avalonia.Dialogs.ManagedFileDialogExtensions.ShowManagedAsync``1(Avalonia.Controls.OpenFileDialog,Avalonia.Controls.Window,Avalonia.Dialogs.ManagedFileDialogOptions) + baseline/Avalonia/lib/net10.0/Avalonia.Dialogs.dll + current/Avalonia/lib/net10.0/Avalonia.Dialogs.dll + + + CP0002 + M:Avalonia.Data.Binding.#ctor(System.String,Avalonia.Data.BindingMode) + baseline/Avalonia/lib/net10.0/Avalonia.Markup.dll + current/Avalonia/lib/net10.0/Avalonia.Markup.dll + + + CP0002 + M:Avalonia.Markup.Xaml.MarkupExtensions.ReflectionBindingExtension.#ctor(System.String,Avalonia.Data.BindingMode) + baseline/Avalonia/lib/net10.0/Avalonia.Markup.Xaml.dll + current/Avalonia/lib/net10.0/Avalonia.Markup.Xaml.dll + + + CP0002 + M:Avalonia.Markup.Xaml.XamlLoadException.#ctor(System.Runtime.Serialization.SerializationInfo,System.Runtime.Serialization.StreamingContext) + baseline/Avalonia/lib/net10.0/Avalonia.Markup.Xaml.dll + current/Avalonia/lib/net10.0/Avalonia.Markup.Xaml.dll + + + CP0002 + M:Avalonia.OpenGL.Egl.EglPlatformSurfaceRenderTargetBase.BeginDraw + baseline/Avalonia/lib/net10.0/Avalonia.OpenGL.dll + current/Avalonia/lib/net10.0/Avalonia.OpenGL.dll + + + CP0002 + M:Avalonia.OpenGL.Egl.EglPlatformSurfaceRenderTargetBase.BeginDraw(System.Nullable{Avalonia.PixelSize}) + baseline/Avalonia/lib/net10.0/Avalonia.OpenGL.dll + current/Avalonia/lib/net10.0/Avalonia.OpenGL.dll + + + CP0002 + M:Avalonia.OpenGL.Egl.EglPlatformSurfaceRenderTargetBase.BeginDrawCore + baseline/Avalonia/lib/net10.0/Avalonia.OpenGL.dll + current/Avalonia/lib/net10.0/Avalonia.OpenGL.dll + + + CP0002 + M:Avalonia.OpenGL.Egl.EglPlatformSurfaceRenderTargetBase.BeginDrawCore(System.Nullable{Avalonia.PixelSize}) + baseline/Avalonia/lib/net10.0/Avalonia.OpenGL.dll + current/Avalonia/lib/net10.0/Avalonia.OpenGL.dll + + + CP0002 + M:Avalonia.OpenGL.IGlPlatformSurfaceRenderTargetFactory.CanRenderToSurface(Avalonia.OpenGL.IGlContext,System.Object) + baseline/Avalonia/lib/net10.0/Avalonia.OpenGL.dll + current/Avalonia/lib/net10.0/Avalonia.OpenGL.dll + + + CP0002 + M:Avalonia.OpenGL.IGlPlatformSurfaceRenderTargetFactory.CreateRenderTarget(Avalonia.OpenGL.IGlContext,System.Object) + baseline/Avalonia/lib/net10.0/Avalonia.OpenGL.dll + current/Avalonia/lib/net10.0/Avalonia.OpenGL.dll + + + CP0002 + M:Avalonia.OpenGL.Surfaces.IGlPlatformSurfaceRenderTarget.BeginDraw + baseline/Avalonia/lib/net10.0/Avalonia.OpenGL.dll + current/Avalonia/lib/net10.0/Avalonia.OpenGL.dll + + + CP0002 + M:Avalonia.OpenGL.Surfaces.IGlPlatformSurfaceRenderTarget.BeginDraw(System.Nullable{Avalonia.PixelSize}) + baseline/Avalonia/lib/net10.0/Avalonia.OpenGL.dll + current/Avalonia/lib/net10.0/Avalonia.OpenGL.dll + + + CP0002 + M:Avalonia.Vulkan.IVulkanKhrSurfacePlatformSurfaceFactory.CanRenderToSurface(Avalonia.Vulkan.IVulkanPlatformGraphicsContext,System.Object) + baseline/Avalonia/lib/net10.0/Avalonia.Vulkan.dll + current/Avalonia/lib/net10.0/Avalonia.Vulkan.dll + + + CP0002 + M:Avalonia.Vulkan.IVulkanKhrSurfacePlatformSurfaceFactory.CreateSurface(Avalonia.Vulkan.IVulkanPlatformGraphicsContext,System.Object) + baseline/Avalonia/lib/net10.0/Avalonia.Vulkan.dll + current/Avalonia/lib/net10.0/Avalonia.Vulkan.dll + + + CP0002 + M:Avalonia.Vulkan.IVulkanPlatformGraphicsContext.CreateRenderTarget(System.Collections.Generic.IEnumerable{System.Object}) + baseline/Avalonia/lib/net10.0/Avalonia.Vulkan.dll + current/Avalonia/lib/net10.0/Avalonia.Vulkan.dll + + + CP0002 + F:Avalonia.Media.Fonts.FontCollectionBase._glyphTypefaceCache + baseline/Avalonia/lib/net6.0/Avalonia.Base.dll + current/Avalonia/lib/net6.0/Avalonia.Base.dll + + CP0002 - F:Avalonia.Controls.TextBox.LetterSpacingProperty - baseline/Avalonia/lib/net10.0/Avalonia.Controls.dll - current/Avalonia/lib/net10.0/Avalonia.Controls.dll + M:Avalonia.Media.Fonts.FontCollectionBase.Initialize(Avalonia.Platform.IFontManagerImpl) + baseline/Avalonia/lib/net6.0/Avalonia.Base.dll + current/Avalonia/lib/net6.0/Avalonia.Base.dll CP0002 - M:Avalonia.AppBuilder.get_LifetimeOverride - baseline/Avalonia/lib/net10.0/Avalonia.Controls.dll - current/Avalonia/lib/net10.0/Avalonia.Controls.dll + M:Avalonia.Media.Fonts.IFontCollection.Initialize(Avalonia.Platform.IFontManagerImpl) + baseline/Avalonia/lib/net6.0/Avalonia.Base.dll + current/Avalonia/lib/net6.0/Avalonia.Base.dll CP0002 - M:Avalonia.Application.add_UrlsOpened(System.EventHandler{Avalonia.UrlOpenedEventArgs}) - baseline/Avalonia/lib/net10.0/Avalonia.Controls.dll - current/Avalonia/lib/net10.0/Avalonia.Controls.dll + M:Avalonia.Platform.IFontManagerImpl.TryMatchCharacter(System.Int32,Avalonia.Media.FontStyle,Avalonia.Media.FontWeight,Avalonia.Media.FontStretch,System.Globalization.CultureInfo,Avalonia.Media.Typeface@) + baseline/Avalonia/lib/net6.0/Avalonia.Base.dll + current/Avalonia/lib/net6.0/Avalonia.Base.dll CP0002 - M:Avalonia.Application.remove_UrlsOpened(System.EventHandler{Avalonia.UrlOpenedEventArgs}) - baseline/Avalonia/lib/net10.0/Avalonia.Controls.dll - current/Avalonia/lib/net10.0/Avalonia.Controls.dll + M:Avalonia.Dialogs.Internal.ManagedFileChooserFilterViewModel.#ctor(Avalonia.Platform.Storage.FilePickerFileType) + baseline/Avalonia/lib/net6.0/Avalonia.Dialogs.dll + current/Avalonia/lib/net6.0/Avalonia.Dialogs.dll CP0002 - M:Avalonia.Controls.ContextMenu.get_PlacementMode - baseline/Avalonia/lib/net10.0/Avalonia.Controls.dll - current/Avalonia/lib/net10.0/Avalonia.Controls.dll + F:Avalonia.Controls.ResourcesChangedEventArgs.Empty + baseline/Avalonia/lib/net8.0/Avalonia.Base.dll + current/Avalonia/lib/net8.0/Avalonia.Base.dll CP0002 - M:Avalonia.Controls.ContextMenu.set_PlacementMode(Avalonia.Controls.PlacementMode) - baseline/Avalonia/lib/net10.0/Avalonia.Controls.dll - current/Avalonia/lib/net10.0/Avalonia.Controls.dll + F:Avalonia.Data.BindingPriority.TemplatedParent + baseline/Avalonia/lib/net8.0/Avalonia.Base.dll + current/Avalonia/lib/net8.0/Avalonia.Base.dll CP0002 - M:Avalonia.Controls.Design.CreatePreviewWithControl(System.Object) - baseline/Avalonia/lib/net10.0/Avalonia.Controls.dll - current/Avalonia/lib/net10.0/Avalonia.Controls.dll + F:Avalonia.Input.DataFormats.FileNames + baseline/Avalonia/lib/net8.0/Avalonia.Base.dll + current/Avalonia/lib/net8.0/Avalonia.Base.dll CP0002 - M:Avalonia.Controls.Design.GetDataContext(Avalonia.Controls.Templates.IDataTemplate) - baseline/Avalonia/lib/net10.0/Avalonia.Controls.dll - current/Avalonia/lib/net10.0/Avalonia.Controls.dll + F:Avalonia.Input.DataFormats.Files + baseline/Avalonia/lib/net8.0/Avalonia.Base.dll + current/Avalonia/lib/net8.0/Avalonia.Base.dll CP0002 - M:Avalonia.Controls.Design.GetPreviewWith(Avalonia.Controls.Templates.IDataTemplate) - baseline/Avalonia/lib/net10.0/Avalonia.Controls.dll - current/Avalonia/lib/net10.0/Avalonia.Controls.dll + F:Avalonia.Input.DataFormats.Text + baseline/Avalonia/lib/net8.0/Avalonia.Base.dll + current/Avalonia/lib/net8.0/Avalonia.Base.dll CP0002 - M:Avalonia.Controls.Design.GetPreviewWith(Avalonia.Styling.IStyle) - baseline/Avalonia/lib/net10.0/Avalonia.Controls.dll - current/Avalonia/lib/net10.0/Avalonia.Controls.dll + F:Avalonia.Input.GestureRecognizers.SwipeGestureRecognizer.CrossAxisCancelThresholdProperty + baseline/Avalonia/lib/net8.0/Avalonia.Base.dll + current/Avalonia/lib/net8.0/Avalonia.Base.dll CP0002 - M:Avalonia.Controls.Design.SetDataContext(Avalonia.Controls.Templates.IDataTemplate,System.Object) - baseline/Avalonia/lib/net10.0/Avalonia.Controls.dll - current/Avalonia/lib/net10.0/Avalonia.Controls.dll + F:Avalonia.Input.GestureRecognizers.SwipeGestureRecognizer.EdgeSizeProperty + baseline/Avalonia/lib/net8.0/Avalonia.Base.dll + current/Avalonia/lib/net8.0/Avalonia.Base.dll CP0002 - M:Avalonia.Controls.Design.SetPreviewWith(Avalonia.AvaloniaObject,Avalonia.Controls.Control) - baseline/Avalonia/lib/net10.0/Avalonia.Controls.dll - current/Avalonia/lib/net10.0/Avalonia.Controls.dll + F:Avalonia.Input.HoldingState.Cancelled + baseline/Avalonia/lib/net8.0/Avalonia.Base.dll + current/Avalonia/lib/net8.0/Avalonia.Base.dll CP0002 - M:Avalonia.Controls.Design.SetPreviewWith(Avalonia.AvaloniaObject,Avalonia.Controls.ITemplate{Avalonia.Controls.Control}) - baseline/Avalonia/lib/net10.0/Avalonia.Controls.dll - current/Avalonia/lib/net10.0/Avalonia.Controls.dll + F:Avalonia.Input.InputElement.GotFocusEvent + baseline/Avalonia/lib/net8.0/Avalonia.Base.dll + current/Avalonia/lib/net8.0/Avalonia.Base.dll CP0002 - M:Avalonia.Controls.Design.SetPreviewWith(Avalonia.Controls.ResourceDictionary,Avalonia.Controls.ITemplate{Avalonia.Controls.Control}) - baseline/Avalonia/lib/net10.0/Avalonia.Controls.dll - current/Avalonia/lib/net10.0/Avalonia.Controls.dll + F:Avalonia.Input.InputElement.LostFocusEvent + baseline/Avalonia/lib/net8.0/Avalonia.Base.dll + current/Avalonia/lib/net8.0/Avalonia.Base.dll CP0002 - M:Avalonia.Controls.Design.SetPreviewWith(Avalonia.Controls.Templates.IDataTemplate,Avalonia.Controls.Control) - baseline/Avalonia/lib/net10.0/Avalonia.Controls.dll - current/Avalonia/lib/net10.0/Avalonia.Controls.dll + F:Avalonia.Media.DrawingImage.ViewboxProperty + baseline/Avalonia/lib/net8.0/Avalonia.Base.dll + current/Avalonia/lib/net8.0/Avalonia.Base.dll CP0002 - M:Avalonia.Controls.Design.SetPreviewWith(Avalonia.Controls.Templates.IDataTemplate,Avalonia.Controls.ITemplate{Avalonia.Controls.Control}) - baseline/Avalonia/lib/net10.0/Avalonia.Controls.dll - current/Avalonia/lib/net10.0/Avalonia.Controls.dll + F:Avalonia.Media.Fonts.FontCollectionBase._glyphTypefaceCache + baseline/Avalonia/lib/net8.0/Avalonia.Base.dll + current/Avalonia/lib/net8.0/Avalonia.Base.dll CP0002 - M:Avalonia.Controls.Design.SetPreviewWith(Avalonia.Styling.IStyle,Avalonia.Controls.Control) - baseline/Avalonia/lib/net10.0/Avalonia.Controls.dll - current/Avalonia/lib/net10.0/Avalonia.Controls.dll + F:Avalonia.Media.RadialGradientBrush.RadiusProperty + baseline/Avalonia/lib/net8.0/Avalonia.Base.dll + current/Avalonia/lib/net8.0/Avalonia.Base.dll CP0002 - M:Avalonia.Controls.Design.SetPreviewWith(Avalonia.Styling.IStyle,Avalonia.Controls.ITemplate{Avalonia.Controls.Control}) - baseline/Avalonia/lib/net10.0/Avalonia.Controls.dll - current/Avalonia/lib/net10.0/Avalonia.Controls.dll + M:Avalonia.Animation.Animation.SetAnimator(Avalonia.Animation.IAnimationSetter,Avalonia.Animation.CustomAnimatorBase) + baseline/Avalonia/lib/net8.0/Avalonia.Base.dll + current/Avalonia/lib/net8.0/Avalonia.Base.dll CP0002 - M:Avalonia.Controls.Generators.ItemContainerGenerator.ContainerFromIndex(System.Int32) - baseline/Avalonia/lib/net10.0/Avalonia.Controls.dll - current/Avalonia/lib/net10.0/Avalonia.Controls.dll + M:Avalonia.AvaloniaObjectExtensions.Bind(Avalonia.AvaloniaObject,Avalonia.AvaloniaProperty,Avalonia.Data.BindingBase,System.Object) + baseline/Avalonia/lib/net8.0/Avalonia.Base.dll + current/Avalonia/lib/net8.0/Avalonia.Base.dll CP0002 - M:Avalonia.Controls.Generators.ItemContainerGenerator.IndexFromContainer(Avalonia.Controls.Control) - baseline/Avalonia/lib/net10.0/Avalonia.Controls.dll - current/Avalonia/lib/net10.0/Avalonia.Controls.dll + M:Avalonia.Controls.ResourcesChangedEventArgs.#ctor + baseline/Avalonia/lib/net8.0/Avalonia.Base.dll + current/Avalonia/lib/net8.0/Avalonia.Base.dll CP0002 - M:Avalonia.Controls.ItemsControl.ItemsControlFromItemContaner(Avalonia.Controls.Control) - baseline/Avalonia/lib/net10.0/Avalonia.Controls.dll - current/Avalonia/lib/net10.0/Avalonia.Controls.dll + M:Avalonia.Data.CompiledBindingPathBuilder.SetRawSource(System.Object) + baseline/Avalonia/lib/net8.0/Avalonia.Base.dll + current/Avalonia/lib/net8.0/Avalonia.Base.dll CP0002 - M:Avalonia.Controls.NativeMenuBar.SetEnableMenuItemClickForwarding(Avalonia.Controls.MenuItem,System.Boolean) - baseline/Avalonia/lib/net10.0/Avalonia.Controls.dll - current/Avalonia/lib/net10.0/Avalonia.Controls.dll + M:Avalonia.Data.CompiledBindingPathBuilder.StreamObservable + baseline/Avalonia/lib/net8.0/Avalonia.Base.dll + current/Avalonia/lib/net8.0/Avalonia.Base.dll CP0002 - M:Avalonia.Controls.NativeMenuItem.get_ToggleType - baseline/Avalonia/lib/net10.0/Avalonia.Controls.dll - current/Avalonia/lib/net10.0/Avalonia.Controls.dll + M:Avalonia.Data.CompiledBindingPathBuilder.StreamTask + baseline/Avalonia/lib/net8.0/Avalonia.Base.dll + current/Avalonia/lib/net8.0/Avalonia.Base.dll CP0002 - M:Avalonia.Controls.Platform.IInsetsManager.get_DisplayEdgeToEdge - baseline/Avalonia/lib/net10.0/Avalonia.Controls.dll - current/Avalonia/lib/net10.0/Avalonia.Controls.dll + M:Avalonia.Data.CompiledBindingPathBuilder.TypeCast(System.Type) + baseline/Avalonia/lib/net8.0/Avalonia.Base.dll + current/Avalonia/lib/net8.0/Avalonia.Base.dll CP0002 - M:Avalonia.Controls.Platform.IInsetsManager.set_DisplayEdgeToEdge(System.Boolean) - baseline/Avalonia/lib/net10.0/Avalonia.Controls.dll - current/Avalonia/lib/net10.0/Avalonia.Controls.dll + M:Avalonia.Data.ReflectionBinding.#ctor(System.String,Avalonia.Data.BindingMode) + baseline/Avalonia/lib/net8.0/Avalonia.Base.dll + current/Avalonia/lib/net8.0/Avalonia.Base.dll CP0002 - M:Avalonia.Controls.Platform.InsetsManagerBase.get_DisplayEdgeToEdge - baseline/Avalonia/lib/net10.0/Avalonia.Controls.dll - current/Avalonia/lib/net10.0/Avalonia.Controls.dll + M:Avalonia.Input.DataObject.Contains(System.String) + baseline/Avalonia/lib/net8.0/Avalonia.Base.dll + current/Avalonia/lib/net8.0/Avalonia.Base.dll CP0002 - M:Avalonia.Controls.Platform.InsetsManagerBase.set_DisplayEdgeToEdge(System.Boolean) - baseline/Avalonia/lib/net10.0/Avalonia.Controls.dll - current/Avalonia/lib/net10.0/Avalonia.Controls.dll + M:Avalonia.Input.DataObject.Get(System.String) + baseline/Avalonia/lib/net8.0/Avalonia.Base.dll + current/Avalonia/lib/net8.0/Avalonia.Base.dll CP0002 - M:Avalonia.Controls.Primitives.AccessText.get_AccessKey - baseline/Avalonia/lib/net10.0/Avalonia.Controls.dll - current/Avalonia/lib/net10.0/Avalonia.Controls.dll + M:Avalonia.Input.DataObject.GetDataFormats + baseline/Avalonia/lib/net8.0/Avalonia.Base.dll + current/Avalonia/lib/net8.0/Avalonia.Base.dll CP0002 - M:Avalonia.Controls.Primitives.OverlayPopupHost.ConfigurePosition(Avalonia.Visual,Avalonia.Controls.PlacementMode,Avalonia.Point,Avalonia.Controls.Primitives.PopupPositioning.PopupAnchor,Avalonia.Controls.Primitives.PopupPositioning.PopupGravity,Avalonia.Controls.Primitives.PopupPositioning.PopupPositionerConstraintAdjustment,System.Nullable{Avalonia.Rect}) - baseline/Avalonia/lib/net10.0/Avalonia.Controls.dll - current/Avalonia/lib/net10.0/Avalonia.Controls.dll + M:Avalonia.Input.DataObject.Set(System.String,System.Object) + baseline/Avalonia/lib/net8.0/Avalonia.Base.dll + current/Avalonia/lib/net8.0/Avalonia.Base.dll CP0002 - M:Avalonia.Controls.Primitives.OverlayPopupHost.CreatePopupHost(Avalonia.Visual,Avalonia.IAvaloniaDependencyResolver) - baseline/Avalonia/lib/net10.0/Avalonia.Controls.dll - current/Avalonia/lib/net10.0/Avalonia.Controls.dll + M:Avalonia.Input.DragDrop.DoDragDrop(Avalonia.Input.PointerEventArgs,Avalonia.Input.IDataObject,Avalonia.Input.DragDropEffects) + baseline/Avalonia/lib/net8.0/Avalonia.Base.dll + current/Avalonia/lib/net8.0/Avalonia.Base.dll CP0002 - M:Avalonia.Controls.Primitives.Popup.get_PlacementMode - baseline/Avalonia/lib/net10.0/Avalonia.Controls.dll - current/Avalonia/lib/net10.0/Avalonia.Controls.dll + M:Avalonia.Input.DragEventArgs.#ctor(Avalonia.Interactivity.RoutedEvent{Avalonia.Input.DragEventArgs},Avalonia.Input.IDataObject,Avalonia.Interactivity.Interactive,Avalonia.Point,Avalonia.Input.KeyModifiers) + baseline/Avalonia/lib/net8.0/Avalonia.Base.dll + current/Avalonia/lib/net8.0/Avalonia.Base.dll CP0002 - M:Avalonia.Controls.Primitives.Popup.set_PlacementMode(Avalonia.Controls.PlacementMode) - baseline/Avalonia/lib/net10.0/Avalonia.Controls.dll - current/Avalonia/lib/net10.0/Avalonia.Controls.dll + M:Avalonia.Input.DragEventArgs.get_Data + baseline/Avalonia/lib/net8.0/Avalonia.Base.dll + current/Avalonia/lib/net8.0/Avalonia.Base.dll CP0002 - M:Avalonia.Controls.Primitives.PopupRoot.ConfigurePosition(Avalonia.Visual,Avalonia.Controls.PlacementMode,Avalonia.Point,Avalonia.Controls.Primitives.PopupPositioning.PopupAnchor,Avalonia.Controls.Primitives.PopupPositioning.PopupGravity,Avalonia.Controls.Primitives.PopupPositioning.PopupPositionerConstraintAdjustment,System.Nullable{Avalonia.Rect}) - baseline/Avalonia/lib/net10.0/Avalonia.Controls.dll - current/Avalonia/lib/net10.0/Avalonia.Controls.dll + M:Avalonia.Input.FocusManager.#ctor(Avalonia.Input.IInputElement) + baseline/Avalonia/lib/net8.0/Avalonia.Base.dll + current/Avalonia/lib/net8.0/Avalonia.Base.dll CP0002 - M:Avalonia.Controls.Primitives.TextSearch.GetText(Avalonia.Controls.Control) - baseline/Avalonia/lib/net10.0/Avalonia.Controls.dll - current/Avalonia/lib/net10.0/Avalonia.Controls.dll + M:Avalonia.Input.FocusManager.ClearFocus + baseline/Avalonia/lib/net8.0/Avalonia.Base.dll + current/Avalonia/lib/net8.0/Avalonia.Base.dll CP0002 - M:Avalonia.Controls.Primitives.TextSearch.SetText(Avalonia.Controls.Control,System.String) - baseline/Avalonia/lib/net10.0/Avalonia.Controls.dll - current/Avalonia/lib/net10.0/Avalonia.Controls.dll + M:Avalonia.Input.FocusManager.ClearFocusOnElementRemoved(Avalonia.Input.IInputElement,Avalonia.Visual) + baseline/Avalonia/lib/net8.0/Avalonia.Base.dll + current/Avalonia/lib/net8.0/Avalonia.Base.dll CP0002 - M:Avalonia.Controls.Primitives.ToggleButton.add_Checked(System.EventHandler{Avalonia.Interactivity.RoutedEventArgs}) - baseline/Avalonia/lib/net10.0/Avalonia.Controls.dll - current/Avalonia/lib/net10.0/Avalonia.Controls.dll + M:Avalonia.Input.FocusManager.FindNextElement(Avalonia.Input.NavigationDirection) + baseline/Avalonia/lib/net8.0/Avalonia.Base.dll + current/Avalonia/lib/net8.0/Avalonia.Base.dll CP0002 - M:Avalonia.Controls.Primitives.ToggleButton.add_Indeterminate(System.EventHandler{Avalonia.Interactivity.RoutedEventArgs}) - baseline/Avalonia/lib/net10.0/Avalonia.Controls.dll - current/Avalonia/lib/net10.0/Avalonia.Controls.dll + M:Avalonia.Input.FocusManager.TryMoveFocus(Avalonia.Input.NavigationDirection) + baseline/Avalonia/lib/net8.0/Avalonia.Base.dll + current/Avalonia/lib/net8.0/Avalonia.Base.dll CP0002 - M:Avalonia.Controls.Primitives.ToggleButton.add_Unchecked(System.EventHandler{Avalonia.Interactivity.RoutedEventArgs}) - baseline/Avalonia/lib/net10.0/Avalonia.Controls.dll - current/Avalonia/lib/net10.0/Avalonia.Controls.dll + M:Avalonia.Input.GestureRecognizers.SwipeGestureRecognizer.get_CrossAxisCancelThreshold + baseline/Avalonia/lib/net8.0/Avalonia.Base.dll + current/Avalonia/lib/net8.0/Avalonia.Base.dll CP0002 - M:Avalonia.Controls.Primitives.ToggleButton.OnChecked(Avalonia.Interactivity.RoutedEventArgs) - baseline/Avalonia/lib/net10.0/Avalonia.Controls.dll - current/Avalonia/lib/net10.0/Avalonia.Controls.dll + M:Avalonia.Input.GestureRecognizers.SwipeGestureRecognizer.get_EdgeSize + baseline/Avalonia/lib/net8.0/Avalonia.Base.dll + current/Avalonia/lib/net8.0/Avalonia.Base.dll CP0002 - M:Avalonia.Controls.Primitives.ToggleButton.OnIndeterminate(Avalonia.Interactivity.RoutedEventArgs) - baseline/Avalonia/lib/net10.0/Avalonia.Controls.dll - current/Avalonia/lib/net10.0/Avalonia.Controls.dll + M:Avalonia.Input.GestureRecognizers.SwipeGestureRecognizer.set_CrossAxisCancelThreshold(System.Double) + baseline/Avalonia/lib/net8.0/Avalonia.Base.dll + current/Avalonia/lib/net8.0/Avalonia.Base.dll CP0002 - M:Avalonia.Controls.Primitives.ToggleButton.OnUnchecked(Avalonia.Interactivity.RoutedEventArgs) - baseline/Avalonia/lib/net10.0/Avalonia.Controls.dll - current/Avalonia/lib/net10.0/Avalonia.Controls.dll + M:Avalonia.Input.GestureRecognizers.SwipeGestureRecognizer.set_EdgeSize(System.Double) + baseline/Avalonia/lib/net8.0/Avalonia.Base.dll + current/Avalonia/lib/net8.0/Avalonia.Base.dll + + + CP0002 + M:Avalonia.Input.HoldingRoutedEventArgs.#ctor(Avalonia.Input.HoldingState,Avalonia.Point,Avalonia.Input.PointerType) + baseline/Avalonia/lib/net8.0/Avalonia.Base.dll + current/Avalonia/lib/net8.0/Avalonia.Base.dll + + + CP0002 + M:Avalonia.Input.IFocusManager.ClearFocus + baseline/Avalonia/lib/net8.0/Avalonia.Base.dll + current/Avalonia/lib/net8.0/Avalonia.Base.dll + + + CP0002 + M:Avalonia.Input.IInputRoot.get_KeyboardNavigationHandler + baseline/Avalonia/lib/net8.0/Avalonia.Base.dll + current/Avalonia/lib/net8.0/Avalonia.Base.dll + + + CP0002 + M:Avalonia.Input.IInputRoot.get_PlatformSettings + baseline/Avalonia/lib/net8.0/Avalonia.Base.dll + current/Avalonia/lib/net8.0/Avalonia.Base.dll + + + CP0002 + M:Avalonia.Input.IInputRoot.get_PointerOverElement + baseline/Avalonia/lib/net8.0/Avalonia.Base.dll + current/Avalonia/lib/net8.0/Avalonia.Base.dll CP0002 - M:Avalonia.Controls.Primitives.ToggleButton.remove_Checked(System.EventHandler{Avalonia.Interactivity.RoutedEventArgs}) - baseline/Avalonia/lib/net10.0/Avalonia.Controls.dll - current/Avalonia/lib/net10.0/Avalonia.Controls.dll + M:Avalonia.Input.IInputRoot.get_ShowAccessKeys + baseline/Avalonia/lib/net8.0/Avalonia.Base.dll + current/Avalonia/lib/net8.0/Avalonia.Base.dll CP0002 - M:Avalonia.Controls.Primitives.ToggleButton.remove_Indeterminate(System.EventHandler{Avalonia.Interactivity.RoutedEventArgs}) - baseline/Avalonia/lib/net10.0/Avalonia.Controls.dll - current/Avalonia/lib/net10.0/Avalonia.Controls.dll + M:Avalonia.Input.IInputRoot.set_PointerOverElement(Avalonia.Input.IInputElement) + baseline/Avalonia/lib/net8.0/Avalonia.Base.dll + current/Avalonia/lib/net8.0/Avalonia.Base.dll CP0002 - M:Avalonia.Controls.Primitives.ToggleButton.remove_Unchecked(System.EventHandler{Avalonia.Interactivity.RoutedEventArgs}) - baseline/Avalonia/lib/net10.0/Avalonia.Controls.dll - current/Avalonia/lib/net10.0/Avalonia.Controls.dll + M:Avalonia.Input.IInputRoot.set_ShowAccessKeys(System.Boolean) + baseline/Avalonia/lib/net8.0/Avalonia.Base.dll + current/Avalonia/lib/net8.0/Avalonia.Base.dll CP0002 - M:Avalonia.Controls.Screens.ScreenFromWindow(Avalonia.Platform.IWindowBaseImpl) - baseline/Avalonia/lib/net10.0/Avalonia.Controls.dll - current/Avalonia/lib/net10.0/Avalonia.Controls.dll + M:Avalonia.Input.IKeyboardNavigationHandler.Move(Avalonia.Input.IInputElement,Avalonia.Input.NavigationDirection,Avalonia.Input.KeyModifiers) + baseline/Avalonia/lib/net8.0/Avalonia.Base.dll + current/Avalonia/lib/net8.0/Avalonia.Base.dll CP0002 - M:Avalonia.Controls.TabItem.SubscribeToOwnerProperties(Avalonia.AvaloniaObject) - baseline/Avalonia/lib/net10.0/Avalonia.Controls.dll - current/Avalonia/lib/net10.0/Avalonia.Controls.dll + M:Avalonia.Input.InputElement.AddPinchEndedHandler(Avalonia.Interactivity.Interactive,System.EventHandler{Avalonia.Input.PinchEndedEventArgs}) + baseline/Avalonia/lib/net8.0/Avalonia.Base.dll + current/Avalonia/lib/net8.0/Avalonia.Base.dll CP0002 - M:Avalonia.Controls.TreeView.get_ItemContainerGenerator - baseline/Avalonia/lib/net10.0/Avalonia.Controls.dll - current/Avalonia/lib/net10.0/Avalonia.Controls.dll + M:Avalonia.Input.InputElement.AddPinchHandler(Avalonia.Interactivity.Interactive,System.EventHandler{Avalonia.Input.PinchEventArgs}) + baseline/Avalonia/lib/net8.0/Avalonia.Base.dll + current/Avalonia/lib/net8.0/Avalonia.Base.dll CP0002 - M:Avalonia.Controls.Window.SortWindowsByZOrder(Avalonia.Controls.Window[]) - baseline/Avalonia/lib/net10.0/Avalonia.Controls.dll - current/Avalonia/lib/net10.0/Avalonia.Controls.dll + M:Avalonia.Input.InputElement.AddPointerTouchPadGestureMagnifyHandler(Avalonia.Interactivity.Interactive,System.EventHandler{Avalonia.Input.PointerDeltaEventArgs}) + baseline/Avalonia/lib/net8.0/Avalonia.Base.dll + current/Avalonia/lib/net8.0/Avalonia.Base.dll CP0002 - M:Avalonia.Platform.IWindowImpl.GetWindowsZOrder(System.Span{Avalonia.Controls.Window},System.Span{System.Int64}) - baseline/Avalonia/lib/net10.0/Avalonia.Controls.dll - current/Avalonia/lib/net10.0/Avalonia.Controls.dll + M:Avalonia.Input.InputElement.AddPointerTouchPadGestureRotateHandler(Avalonia.Interactivity.Interactive,System.EventHandler{Avalonia.Input.PointerDeltaEventArgs}) + baseline/Avalonia/lib/net8.0/Avalonia.Base.dll + current/Avalonia/lib/net8.0/Avalonia.Base.dll CP0002 - M:Avalonia.Platform.Screen.#ctor(System.Double,Avalonia.PixelRect,Avalonia.PixelRect,System.Boolean) - baseline/Avalonia/lib/net10.0/Avalonia.Controls.dll - current/Avalonia/lib/net10.0/Avalonia.Controls.dll + M:Avalonia.Input.InputElement.AddPointerTouchPadGestureSwipeHandler(Avalonia.Interactivity.Interactive,System.EventHandler{Avalonia.Input.PointerDeltaEventArgs}) + baseline/Avalonia/lib/net8.0/Avalonia.Base.dll + current/Avalonia/lib/net8.0/Avalonia.Base.dll CP0002 - M:Avalonia.Platform.Screen.get_PixelDensity - baseline/Avalonia/lib/net10.0/Avalonia.Controls.dll - current/Avalonia/lib/net10.0/Avalonia.Controls.dll + M:Avalonia.Input.InputElement.AddPullGestureEndedHandler(Avalonia.Interactivity.Interactive,System.EventHandler{Avalonia.Input.PullGestureEndedEventArgs}) + baseline/Avalonia/lib/net8.0/Avalonia.Base.dll + current/Avalonia/lib/net8.0/Avalonia.Base.dll CP0002 - M:Avalonia.Platform.Screen.get_Primary - baseline/Avalonia/lib/net10.0/Avalonia.Controls.dll - current/Avalonia/lib/net10.0/Avalonia.Controls.dll + M:Avalonia.Input.InputElement.AddPullGestureHandler(Avalonia.Interactivity.Interactive,System.EventHandler{Avalonia.Input.PullGestureEventArgs}) + baseline/Avalonia/lib/net8.0/Avalonia.Base.dll + current/Avalonia/lib/net8.0/Avalonia.Base.dll CP0002 - M:Avalonia.Platform.Screen.set_Bounds(Avalonia.PixelRect) - baseline/Avalonia/lib/net10.0/Avalonia.Controls.dll - current/Avalonia/lib/net10.0/Avalonia.Controls.dll + M:Avalonia.Input.InputElement.AddScrollGestureEndedHandler(Avalonia.Interactivity.Interactive,System.EventHandler{Avalonia.Input.ScrollGestureEndedEventArgs}) + baseline/Avalonia/lib/net8.0/Avalonia.Base.dll + current/Avalonia/lib/net8.0/Avalonia.Base.dll CP0002 - M:Avalonia.Platform.Screen.set_CurrentOrientation(Avalonia.Platform.ScreenOrientation) - baseline/Avalonia/lib/net10.0/Avalonia.Controls.dll - current/Avalonia/lib/net10.0/Avalonia.Controls.dll + M:Avalonia.Input.InputElement.AddScrollGestureHandler(Avalonia.Interactivity.Interactive,System.EventHandler{Avalonia.Input.ScrollGestureEventArgs}) + baseline/Avalonia/lib/net8.0/Avalonia.Base.dll + current/Avalonia/lib/net8.0/Avalonia.Base.dll CP0002 - M:Avalonia.Platform.Screen.set_DisplayName(System.String) - baseline/Avalonia/lib/net10.0/Avalonia.Controls.dll - current/Avalonia/lib/net10.0/Avalonia.Controls.dll + M:Avalonia.Input.InputElement.AddScrollGestureInertiaStartingHandler(Avalonia.Interactivity.Interactive,System.EventHandler{Avalonia.Input.ScrollGestureInertiaStartingEventArgs}) + baseline/Avalonia/lib/net8.0/Avalonia.Base.dll + current/Avalonia/lib/net8.0/Avalonia.Base.dll CP0002 - M:Avalonia.Platform.Screen.set_IsPrimary(System.Boolean) - baseline/Avalonia/lib/net10.0/Avalonia.Controls.dll - current/Avalonia/lib/net10.0/Avalonia.Controls.dll + M:Avalonia.Input.InputElement.OnGotFocus(Avalonia.Input.GotFocusEventArgs) + baseline/Avalonia/lib/net8.0/Avalonia.Base.dll + current/Avalonia/lib/net8.0/Avalonia.Base.dll CP0002 - M:Avalonia.Platform.Screen.set_Scaling(System.Double) - baseline/Avalonia/lib/net10.0/Avalonia.Controls.dll - current/Avalonia/lib/net10.0/Avalonia.Controls.dll + M:Avalonia.Input.InputElement.OnLostFocus(Avalonia.Interactivity.RoutedEventArgs) + baseline/Avalonia/lib/net8.0/Avalonia.Base.dll + current/Avalonia/lib/net8.0/Avalonia.Base.dll CP0002 - M:Avalonia.Platform.Screen.set_WorkingArea(Avalonia.PixelRect) - baseline/Avalonia/lib/net10.0/Avalonia.Controls.dll - current/Avalonia/lib/net10.0/Avalonia.Controls.dll + M:Avalonia.Input.InputElement.RemovePinchEndedHandler(Avalonia.Interactivity.Interactive,System.EventHandler{Avalonia.Input.PinchEndedEventArgs}) + baseline/Avalonia/lib/net8.0/Avalonia.Base.dll + current/Avalonia/lib/net8.0/Avalonia.Base.dll CP0002 - M:Avalonia.Dialogs.ManagedFileDialogExtensions.ShowManagedAsync(Avalonia.Controls.OpenFileDialog,Avalonia.Controls.Window,Avalonia.Dialogs.ManagedFileDialogOptions) - baseline/Avalonia/lib/net10.0/Avalonia.Dialogs.dll - current/Avalonia/lib/net10.0/Avalonia.Dialogs.dll + M:Avalonia.Input.InputElement.RemovePinchHandler(Avalonia.Interactivity.Interactive,System.EventHandler{Avalonia.Input.PinchEventArgs}) + baseline/Avalonia/lib/net8.0/Avalonia.Base.dll + current/Avalonia/lib/net8.0/Avalonia.Base.dll CP0002 - M:Avalonia.Dialogs.ManagedFileDialogExtensions.ShowManagedAsync``1(Avalonia.Controls.OpenFileDialog,Avalonia.Controls.Window,Avalonia.Dialogs.ManagedFileDialogOptions) - baseline/Avalonia/lib/net10.0/Avalonia.Dialogs.dll - current/Avalonia/lib/net10.0/Avalonia.Dialogs.dll + M:Avalonia.Input.InputElement.RemovePointerTouchPadGestureMagnifyHandler(Avalonia.Interactivity.Interactive,System.EventHandler{Avalonia.Input.PointerDeltaEventArgs}) + baseline/Avalonia/lib/net8.0/Avalonia.Base.dll + current/Avalonia/lib/net8.0/Avalonia.Base.dll CP0002 - M:Avalonia.Data.Binding.#ctor(System.String,Avalonia.Data.BindingMode) - baseline/Avalonia/lib/net10.0/Avalonia.Markup.dll - current/Avalonia/lib/net10.0/Avalonia.Markup.dll + M:Avalonia.Input.InputElement.RemovePointerTouchPadGestureRotateHandler(Avalonia.Interactivity.Interactive,System.EventHandler{Avalonia.Input.PointerDeltaEventArgs}) + baseline/Avalonia/lib/net8.0/Avalonia.Base.dll + current/Avalonia/lib/net8.0/Avalonia.Base.dll CP0002 - M:Avalonia.Markup.Xaml.MarkupExtensions.ReflectionBindingExtension.#ctor(System.String,Avalonia.Data.BindingMode) - baseline/Avalonia/lib/net10.0/Avalonia.Markup.Xaml.dll - current/Avalonia/lib/net10.0/Avalonia.Markup.Xaml.dll + M:Avalonia.Input.InputElement.RemovePointerTouchPadGestureSwipeHandler(Avalonia.Interactivity.Interactive,System.EventHandler{Avalonia.Input.PointerDeltaEventArgs}) + baseline/Avalonia/lib/net8.0/Avalonia.Base.dll + current/Avalonia/lib/net8.0/Avalonia.Base.dll CP0002 - M:Avalonia.Markup.Xaml.XamlLoadException.#ctor(System.Runtime.Serialization.SerializationInfo,System.Runtime.Serialization.StreamingContext) - baseline/Avalonia/lib/net10.0/Avalonia.Markup.Xaml.dll - current/Avalonia/lib/net10.0/Avalonia.Markup.Xaml.dll + M:Avalonia.Input.InputElement.RemovePullGestureEndedHandler(Avalonia.Interactivity.Interactive,System.EventHandler{Avalonia.Input.PullGestureEndedEventArgs}) + baseline/Avalonia/lib/net8.0/Avalonia.Base.dll + current/Avalonia/lib/net8.0/Avalonia.Base.dll CP0002 - M:Avalonia.OpenGL.Egl.EglPlatformSurfaceRenderTargetBase.BeginDraw - baseline/Avalonia/lib/net10.0/Avalonia.OpenGL.dll - current/Avalonia/lib/net10.0/Avalonia.OpenGL.dll + M:Avalonia.Input.InputElement.RemovePullGestureHandler(Avalonia.Interactivity.Interactive,System.EventHandler{Avalonia.Input.PullGestureEventArgs}) + baseline/Avalonia/lib/net8.0/Avalonia.Base.dll + current/Avalonia/lib/net8.0/Avalonia.Base.dll CP0002 - M:Avalonia.OpenGL.Egl.EglPlatformSurfaceRenderTargetBase.BeginDrawCore - baseline/Avalonia/lib/net10.0/Avalonia.OpenGL.dll - current/Avalonia/lib/net10.0/Avalonia.OpenGL.dll + M:Avalonia.Input.InputElement.RemoveScrollGestureEndedHandler(Avalonia.Interactivity.Interactive,System.EventHandler{Avalonia.Input.ScrollGestureEndedEventArgs}) + baseline/Avalonia/lib/net8.0/Avalonia.Base.dll + current/Avalonia/lib/net8.0/Avalonia.Base.dll CP0002 - M:Avalonia.OpenGL.Surfaces.IGlPlatformSurfaceRenderTarget.BeginDraw - baseline/Avalonia/lib/net10.0/Avalonia.OpenGL.dll - current/Avalonia/lib/net10.0/Avalonia.OpenGL.dll + M:Avalonia.Input.InputElement.RemoveScrollGestureHandler(Avalonia.Interactivity.Interactive,System.EventHandler{Avalonia.Input.ScrollGestureEventArgs}) + baseline/Avalonia/lib/net8.0/Avalonia.Base.dll + current/Avalonia/lib/net8.0/Avalonia.Base.dll CP0002 - F:Avalonia.Media.Fonts.FontCollectionBase._glyphTypefaceCache - baseline/Avalonia/lib/net6.0/Avalonia.Base.dll - current/Avalonia/lib/net6.0/Avalonia.Base.dll + M:Avalonia.Input.InputElement.RemoveScrollGestureInertiaStartingHandler(Avalonia.Interactivity.Interactive,System.EventHandler{Avalonia.Input.ScrollGestureInertiaStartingEventArgs}) + baseline/Avalonia/lib/net8.0/Avalonia.Base.dll + current/Avalonia/lib/net8.0/Avalonia.Base.dll CP0002 - M:Avalonia.Media.Fonts.FontCollectionBase.Initialize(Avalonia.Platform.IFontManagerImpl) - baseline/Avalonia/lib/net6.0/Avalonia.Base.dll - current/Avalonia/lib/net6.0/Avalonia.Base.dll + M:Avalonia.Input.KeyboardNavigationHandler.Move(Avalonia.Input.IInputElement,Avalonia.Input.NavigationDirection,Avalonia.Input.KeyModifiers) + baseline/Avalonia/lib/net8.0/Avalonia.Base.dll + current/Avalonia/lib/net8.0/Avalonia.Base.dll CP0002 - M:Avalonia.Media.Fonts.IFontCollection.Initialize(Avalonia.Platform.IFontManagerImpl) - baseline/Avalonia/lib/net6.0/Avalonia.Base.dll - current/Avalonia/lib/net6.0/Avalonia.Base.dll + M:Avalonia.Input.Platform.IClipboard.GetDataAsync(System.String) + baseline/Avalonia/lib/net8.0/Avalonia.Base.dll + current/Avalonia/lib/net8.0/Avalonia.Base.dll CP0002 - M:Avalonia.Platform.IFontManagerImpl.TryMatchCharacter(System.Int32,Avalonia.Media.FontStyle,Avalonia.Media.FontWeight,Avalonia.Media.FontStretch,System.Globalization.CultureInfo,Avalonia.Media.Typeface@) - baseline/Avalonia/lib/net6.0/Avalonia.Base.dll - current/Avalonia/lib/net6.0/Avalonia.Base.dll + M:Avalonia.Input.Platform.IClipboard.GetFormatsAsync + baseline/Avalonia/lib/net8.0/Avalonia.Base.dll + current/Avalonia/lib/net8.0/Avalonia.Base.dll CP0002 - M:Avalonia.Dialogs.Internal.ManagedFileChooserFilterViewModel.#ctor(Avalonia.Platform.Storage.FilePickerFileType) - baseline/Avalonia/lib/net6.0/Avalonia.Dialogs.dll - current/Avalonia/lib/net6.0/Avalonia.Dialogs.dll + M:Avalonia.Input.Platform.IClipboard.GetTextAsync + baseline/Avalonia/lib/net8.0/Avalonia.Base.dll + current/Avalonia/lib/net8.0/Avalonia.Base.dll CP0002 - F:Avalonia.Controls.ResourcesChangedEventArgs.Empty + M:Avalonia.Input.Platform.IClipboard.SetDataObjectAsync(Avalonia.Input.IDataObject) baseline/Avalonia/lib/net8.0/Avalonia.Base.dll current/Avalonia/lib/net8.0/Avalonia.Base.dll CP0002 - F:Avalonia.Data.BindingPriority.TemplatedParent + M:Avalonia.Input.Platform.IClipboard.SetTextAsync(System.String) baseline/Avalonia/lib/net8.0/Avalonia.Base.dll current/Avalonia/lib/net8.0/Avalonia.Base.dll CP0002 - F:Avalonia.Input.DataFormats.FileNames + M:Avalonia.Input.Platform.IClipboard.TryGetInProcessDataObjectAsync baseline/Avalonia/lib/net8.0/Avalonia.Base.dll current/Avalonia/lib/net8.0/Avalonia.Base.dll CP0002 - F:Avalonia.Input.DataFormats.Files + M:Avalonia.Input.Platform.IPlatformDragSource.DoDragDrop(Avalonia.Input.PointerEventArgs,Avalonia.Input.IDataObject,Avalonia.Input.DragDropEffects) baseline/Avalonia/lib/net8.0/Avalonia.Base.dll current/Avalonia/lib/net8.0/Avalonia.Base.dll CP0002 - F:Avalonia.Input.DataFormats.Text + M:Avalonia.Input.Raw.RawDragEvent.#ctor(Avalonia.Input.Raw.IDragDropDevice,Avalonia.Input.Raw.RawDragEventType,Avalonia.Input.IInputRoot,Avalonia.Point,Avalonia.Input.IDataObject,Avalonia.Input.DragDropEffects,Avalonia.Input.RawInputModifiers) baseline/Avalonia/lib/net8.0/Avalonia.Base.dll current/Avalonia/lib/net8.0/Avalonia.Base.dll CP0002 - F:Avalonia.Media.DrawingImage.ViewboxProperty + M:Avalonia.Input.Raw.RawDragEvent.get_Data baseline/Avalonia/lib/net8.0/Avalonia.Base.dll current/Avalonia/lib/net8.0/Avalonia.Base.dll CP0002 - F:Avalonia.Media.Fonts.FontCollectionBase._glyphTypefaceCache + M:Avalonia.Input.Raw.RawKeyEventArgs.#ctor(Avalonia.Input.IInputDevice,System.UInt64,Avalonia.Input.IInputRoot,Avalonia.Input.Raw.RawKeyEventType,Avalonia.Input.Key,Avalonia.Input.RawInputModifiers,Avalonia.Input.PhysicalKey,Avalonia.Input.KeyDeviceType,System.String) baseline/Avalonia/lib/net8.0/Avalonia.Base.dll current/Avalonia/lib/net8.0/Avalonia.Base.dll CP0002 - F:Avalonia.Media.RadialGradientBrush.RadiusProperty + M:Avalonia.Input.Raw.RawKeyEventArgs.#ctor(Avalonia.Input.IInputDevice,System.UInt64,Avalonia.Input.IInputRoot,Avalonia.Input.Raw.RawKeyEventType,Avalonia.Input.Key,Avalonia.Input.RawInputModifiers,Avalonia.Input.PhysicalKey,System.String) baseline/Avalonia/lib/net8.0/Avalonia.Base.dll current/Avalonia/lib/net8.0/Avalonia.Base.dll CP0002 - M:Avalonia.Animation.Animation.SetAnimator(Avalonia.Animation.IAnimationSetter,Avalonia.Animation.CustomAnimatorBase) + M:Avalonia.Input.Raw.RawKeyEventArgs.#ctor(Avalonia.Input.IKeyboardDevice,System.UInt64,Avalonia.Input.IInputRoot,Avalonia.Input.Raw.RawKeyEventType,Avalonia.Input.Key,Avalonia.Input.RawInputModifiers) baseline/Avalonia/lib/net8.0/Avalonia.Base.dll current/Avalonia/lib/net8.0/Avalonia.Base.dll CP0002 - M:Avalonia.AvaloniaObjectExtensions.Bind(Avalonia.AvaloniaObject,Avalonia.AvaloniaProperty,Avalonia.Data.BindingBase,System.Object) + M:Avalonia.Input.SwipeGestureEventArgs.#ctor(System.Int32,Avalonia.Input.SwipeDirection,Avalonia.Vector,Avalonia.Point) baseline/Avalonia/lib/net8.0/Avalonia.Base.dll current/Avalonia/lib/net8.0/Avalonia.Base.dll CP0002 - M:Avalonia.Controls.ResourcesChangedEventArgs.#ctor + M:Avalonia.Input.SwipeGestureEventArgs.get_StartPoint baseline/Avalonia/lib/net8.0/Avalonia.Base.dll current/Avalonia/lib/net8.0/Avalonia.Base.dll CP0002 - M:Avalonia.Data.CompiledBindingPathBuilder.SetRawSource(System.Object) + M:Avalonia.Input.TextInput.TextInputMethodClient.ShowInputPanel baseline/Avalonia/lib/net8.0/Avalonia.Base.dll current/Avalonia/lib/net8.0/Avalonia.Base.dll CP0002 - M:Avalonia.Data.ReflectionBinding.#ctor(System.String,Avalonia.Data.BindingMode) + M:Avalonia.Layout.LayoutHelper.RoundLayoutSizeUp(Avalonia.Size,System.Double,System.Double) baseline/Avalonia/lib/net8.0/Avalonia.Base.dll current/Avalonia/lib/net8.0/Avalonia.Base.dll CP0002 - M:Avalonia.Input.DataObject.Contains(System.String) + M:Avalonia.Layout.LayoutHelper.RoundLayoutThickness(Avalonia.Thickness,System.Double,System.Double) baseline/Avalonia/lib/net8.0/Avalonia.Base.dll current/Avalonia/lib/net8.0/Avalonia.Base.dll CP0002 - M:Avalonia.Input.DataObject.Get(System.String) + M:Avalonia.Media.Color.ToUint32 baseline/Avalonia/lib/net8.0/Avalonia.Base.dll current/Avalonia/lib/net8.0/Avalonia.Base.dll CP0002 - M:Avalonia.Input.DataObject.GetDataFormats + M:Avalonia.Media.DrawingContext.PushPostTransform(Avalonia.Matrix) baseline/Avalonia/lib/net8.0/Avalonia.Base.dll current/Avalonia/lib/net8.0/Avalonia.Base.dll CP0002 - M:Avalonia.Input.DataObject.Set(System.String,System.Object) + M:Avalonia.Media.DrawingContext.PushPreTransform(Avalonia.Matrix) baseline/Avalonia/lib/net8.0/Avalonia.Base.dll current/Avalonia/lib/net8.0/Avalonia.Base.dll CP0002 - M:Avalonia.Input.DragDrop.DoDragDrop(Avalonia.Input.PointerEventArgs,Avalonia.Input.IDataObject,Avalonia.Input.DragDropEffects) + M:Avalonia.Media.DrawingContext.PushTransformContainer baseline/Avalonia/lib/net8.0/Avalonia.Base.dll current/Avalonia/lib/net8.0/Avalonia.Base.dll CP0002 - M:Avalonia.Input.DragEventArgs.#ctor(Avalonia.Interactivity.RoutedEvent{Avalonia.Input.DragEventArgs},Avalonia.Input.IDataObject,Avalonia.Interactivity.Interactive,Avalonia.Point,Avalonia.Input.KeyModifiers) + M:Avalonia.Media.DrawingImage.get_Viewbox baseline/Avalonia/lib/net8.0/Avalonia.Base.dll current/Avalonia/lib/net8.0/Avalonia.Base.dll CP0002 - M:Avalonia.Input.DragEventArgs.get_Data + M:Avalonia.Media.DrawingImage.set_Viewbox(System.Nullable{Avalonia.Rect}) baseline/Avalonia/lib/net8.0/Avalonia.Base.dll current/Avalonia/lib/net8.0/Avalonia.Base.dll CP0002 - M:Avalonia.Input.IKeyboardNavigationHandler.Move(Avalonia.Input.IInputElement,Avalonia.Input.NavigationDirection,Avalonia.Input.KeyModifiers) + M:Avalonia.Media.Fonts.FontCollectionBase.Initialize(Avalonia.Platform.IFontManagerImpl) baseline/Avalonia/lib/net8.0/Avalonia.Base.dll current/Avalonia/lib/net8.0/Avalonia.Base.dll CP0002 - M:Avalonia.Input.KeyboardNavigationHandler.Move(Avalonia.Input.IInputElement,Avalonia.Input.NavigationDirection,Avalonia.Input.KeyModifiers) + M:Avalonia.Media.Fonts.FontCollectionBase.TryGetGlyphTypeface(System.String,Avalonia.Media.Fonts.FontCollectionKey,Avalonia.Media.GlyphTypeface@) baseline/Avalonia/lib/net8.0/Avalonia.Base.dll current/Avalonia/lib/net8.0/Avalonia.Base.dll CP0002 - M:Avalonia.Input.Platform.IClipboard.GetDataAsync(System.String) + M:Avalonia.Media.Fonts.IFontCollection.Initialize(Avalonia.Platform.IFontManagerImpl) baseline/Avalonia/lib/net8.0/Avalonia.Base.dll current/Avalonia/lib/net8.0/Avalonia.Base.dll CP0002 - M:Avalonia.Input.Platform.IClipboard.GetFormatsAsync + M:Avalonia.Media.Imaging.Bitmap.CopyPixels(Avalonia.Platform.ILockedFramebuffer,Avalonia.Platform.AlphaFormat) baseline/Avalonia/lib/net8.0/Avalonia.Base.dll current/Avalonia/lib/net8.0/Avalonia.Base.dll CP0002 - M:Avalonia.Input.Platform.IClipboard.GetTextAsync + M:Avalonia.Media.Immutable.ImmutableRadialGradientBrush.get_Radius baseline/Avalonia/lib/net8.0/Avalonia.Base.dll current/Avalonia/lib/net8.0/Avalonia.Base.dll CP0002 - M:Avalonia.Input.Platform.IClipboard.SetDataObjectAsync(Avalonia.Input.IDataObject) + M:Avalonia.Media.IRadialGradientBrush.get_Radius baseline/Avalonia/lib/net8.0/Avalonia.Base.dll current/Avalonia/lib/net8.0/Avalonia.Base.dll CP0002 - M:Avalonia.Input.Platform.IClipboard.SetTextAsync(System.String) + M:Avalonia.Media.RadialGradientBrush.get_Radius baseline/Avalonia/lib/net8.0/Avalonia.Base.dll current/Avalonia/lib/net8.0/Avalonia.Base.dll CP0002 - M:Avalonia.Input.Platform.IClipboard.TryGetInProcessDataObjectAsync + M:Avalonia.Media.RadialGradientBrush.set_Radius(System.Double) baseline/Avalonia/lib/net8.0/Avalonia.Base.dll current/Avalonia/lib/net8.0/Avalonia.Base.dll CP0002 - M:Avalonia.Input.Platform.IPlatformDragSource.DoDragDrop(Avalonia.Input.PointerEventArgs,Avalonia.Input.IDataObject,Avalonia.Input.DragDropEffects) + M:Avalonia.Media.StreamGeometryContext.ArcTo(Avalonia.Point,Avalonia.Size,System.Double,System.Boolean,Avalonia.Media.SweepDirection) baseline/Avalonia/lib/net8.0/Avalonia.Base.dll current/Avalonia/lib/net8.0/Avalonia.Base.dll CP0002 - M:Avalonia.Input.Raw.RawDragEvent.#ctor(Avalonia.Input.Raw.IDragDropDevice,Avalonia.Input.Raw.RawDragEventType,Avalonia.Input.IInputRoot,Avalonia.Point,Avalonia.Input.IDataObject,Avalonia.Input.DragDropEffects,Avalonia.Input.RawInputModifiers) + M:Avalonia.Media.StreamGeometryContext.CubicBezierTo(Avalonia.Point,Avalonia.Point,Avalonia.Point) baseline/Avalonia/lib/net8.0/Avalonia.Base.dll current/Avalonia/lib/net8.0/Avalonia.Base.dll CP0002 - M:Avalonia.Input.Raw.RawDragEvent.get_Data + M:Avalonia.Media.StreamGeometryContext.LineTo(Avalonia.Point) baseline/Avalonia/lib/net8.0/Avalonia.Base.dll current/Avalonia/lib/net8.0/Avalonia.Base.dll CP0002 - M:Avalonia.Input.Raw.RawKeyEventArgs.#ctor(Avalonia.Input.IInputDevice,System.UInt64,Avalonia.Input.IInputRoot,Avalonia.Input.Raw.RawKeyEventType,Avalonia.Input.Key,Avalonia.Input.RawInputModifiers,Avalonia.Input.PhysicalKey,Avalonia.Input.KeyDeviceType,System.String) + M:Avalonia.Media.StreamGeometryContext.QuadraticBezierTo(Avalonia.Point,Avalonia.Point) baseline/Avalonia/lib/net8.0/Avalonia.Base.dll current/Avalonia/lib/net8.0/Avalonia.Base.dll CP0002 - M:Avalonia.Input.Raw.RawKeyEventArgs.#ctor(Avalonia.Input.IInputDevice,System.UInt64,Avalonia.Input.IInputRoot,Avalonia.Input.Raw.RawKeyEventType,Avalonia.Input.Key,Avalonia.Input.RawInputModifiers,Avalonia.Input.PhysicalKey,System.String) + M:Avalonia.Media.TextFormatting.GenericTextRunProperties.#ctor(Avalonia.Media.Typeface,Avalonia.Media.FontFeatureCollection,System.Double,Avalonia.Media.TextDecorationCollection,Avalonia.Media.IBrush,Avalonia.Media.IBrush,Avalonia.Media.BaselineAlignment,System.Globalization.CultureInfo) baseline/Avalonia/lib/net8.0/Avalonia.Base.dll current/Avalonia/lib/net8.0/Avalonia.Base.dll CP0002 - M:Avalonia.Input.Raw.RawKeyEventArgs.#ctor(Avalonia.Input.IKeyboardDevice,System.UInt64,Avalonia.Input.IInputRoot,Avalonia.Input.Raw.RawKeyEventType,Avalonia.Input.Key,Avalonia.Input.RawInputModifiers) + M:Avalonia.Media.TextFormatting.GenericTextRunProperties.#ctor(Avalonia.Media.Typeface,System.Double,Avalonia.Media.TextDecorationCollection,Avalonia.Media.IBrush,Avalonia.Media.IBrush,Avalonia.Media.BaselineAlignment,System.Globalization.CultureInfo) baseline/Avalonia/lib/net8.0/Avalonia.Base.dll current/Avalonia/lib/net8.0/Avalonia.Base.dll CP0002 - M:Avalonia.Input.TextInput.TextInputMethodClient.ShowInputPanel + M:Avalonia.Media.TextFormatting.TextCollapsingProperties.CreateCollapsedRuns(Avalonia.Media.TextFormatting.TextLine,System.Int32,Avalonia.Media.FlowDirection,Avalonia.Media.TextFormatting.TextRun) baseline/Avalonia/lib/net8.0/Avalonia.Base.dll current/Avalonia/lib/net8.0/Avalonia.Base.dll CP0002 - M:Avalonia.Media.Color.ToUint32 + M:Avalonia.Media.TextFormatting.TextLayout.#ctor(System.String,Avalonia.Media.Typeface,Avalonia.Media.FontFeatureCollection,System.Double,Avalonia.Media.IBrush,Avalonia.Media.TextAlignment,Avalonia.Media.TextWrapping,Avalonia.Media.TextTrimming,Avalonia.Media.TextDecorationCollection,Avalonia.Media.FlowDirection,System.Double,System.Double,System.Double,System.Double,System.Int32,System.Collections.Generic.IReadOnlyList{Avalonia.Utilities.ValueSpan{Avalonia.Media.TextFormatting.TextRunProperties}}) baseline/Avalonia/lib/net8.0/Avalonia.Base.dll current/Avalonia/lib/net8.0/Avalonia.Base.dll CP0002 - M:Avalonia.Media.DrawingContext.PushPostTransform(Avalonia.Matrix) + M:Avalonia.Media.TextFormatting.TextLayout.#ctor(System.String,Avalonia.Media.Typeface,System.Double,Avalonia.Media.IBrush,Avalonia.Media.TextAlignment,Avalonia.Media.TextWrapping,Avalonia.Media.TextTrimming,Avalonia.Media.TextDecorationCollection,Avalonia.Media.FlowDirection,System.Double,System.Double,System.Double,System.Double,System.Int32,System.Collections.Generic.IReadOnlyList{Avalonia.Utilities.ValueSpan{Avalonia.Media.TextFormatting.TextRunProperties}}) baseline/Avalonia/lib/net8.0/Avalonia.Base.dll current/Avalonia/lib/net8.0/Avalonia.Base.dll CP0002 - M:Avalonia.Media.DrawingContext.PushPreTransform(Avalonia.Matrix) + M:Avalonia.Media.TextFormatting.TextShaperOptions.#ctor(Avalonia.Media.GlyphTypeface,System.Collections.Generic.IReadOnlyList{Avalonia.Media.FontFeature},System.Double,System.SByte,System.Globalization.CultureInfo,System.Double,System.Double) baseline/Avalonia/lib/net8.0/Avalonia.Base.dll current/Avalonia/lib/net8.0/Avalonia.Base.dll CP0002 - M:Avalonia.Media.DrawingContext.PushTransformContainer + M:Avalonia.Media.TextFormatting.TextShaperOptions.#ctor(Avalonia.Media.GlyphTypeface,System.Double,System.SByte,System.Globalization.CultureInfo,System.Double,System.Double) baseline/Avalonia/lib/net8.0/Avalonia.Base.dll current/Avalonia/lib/net8.0/Avalonia.Base.dll CP0002 - M:Avalonia.Media.DrawingImage.get_Viewbox + M:Avalonia.Platform.ICursorFactory.CreateCursor(Avalonia.Platform.IBitmapImpl,Avalonia.PixelPoint) baseline/Avalonia/lib/net8.0/Avalonia.Base.dll current/Avalonia/lib/net8.0/Avalonia.Base.dll CP0002 - M:Avalonia.Media.DrawingImage.set_Viewbox(System.Nullable{Avalonia.Rect}) + M:Avalonia.Platform.IDrawingContextImplWithEffects.PushEffect(Avalonia.Media.IEffect) baseline/Avalonia/lib/net8.0/Avalonia.Base.dll current/Avalonia/lib/net8.0/Avalonia.Base.dll CP0002 - M:Avalonia.Media.Fonts.FontCollectionBase.Initialize(Avalonia.Platform.IFontManagerImpl) + M:Avalonia.Platform.IFontManagerImpl.TryMatchCharacter(System.Int32,Avalonia.Media.FontStyle,Avalonia.Media.FontWeight,Avalonia.Media.FontStretch,System.Globalization.CultureInfo,Avalonia.Media.Typeface@) baseline/Avalonia/lib/net8.0/Avalonia.Base.dll current/Avalonia/lib/net8.0/Avalonia.Base.dll CP0002 - M:Avalonia.Media.Fonts.IFontCollection.Initialize(Avalonia.Platform.IFontManagerImpl) + M:Avalonia.Platform.IGeometryContext.ArcTo(Avalonia.Point,Avalonia.Size,System.Double,System.Boolean,Avalonia.Media.SweepDirection) baseline/Avalonia/lib/net8.0/Avalonia.Base.dll current/Avalonia/lib/net8.0/Avalonia.Base.dll CP0002 - M:Avalonia.Media.Imaging.Bitmap.CopyPixels(Avalonia.Platform.ILockedFramebuffer,Avalonia.Platform.AlphaFormat) + M:Avalonia.Platform.IGeometryContext.CubicBezierTo(Avalonia.Point,Avalonia.Point,Avalonia.Point) baseline/Avalonia/lib/net8.0/Avalonia.Base.dll current/Avalonia/lib/net8.0/Avalonia.Base.dll CP0002 - M:Avalonia.Media.Immutable.ImmutableRadialGradientBrush.get_Radius + M:Avalonia.Platform.IGeometryContext.LineTo(Avalonia.Point) baseline/Avalonia/lib/net8.0/Avalonia.Base.dll current/Avalonia/lib/net8.0/Avalonia.Base.dll CP0002 - M:Avalonia.Media.IRadialGradientBrush.get_Radius + M:Avalonia.Platform.IGeometryContext.QuadraticBezierTo(Avalonia.Point,Avalonia.Point) baseline/Avalonia/lib/net8.0/Avalonia.Base.dll current/Avalonia/lib/net8.0/Avalonia.Base.dll CP0002 - M:Avalonia.Media.RadialGradientBrush.get_Radius + M:Avalonia.Platform.IPlatformRenderInterfaceContext.CreateOffscreenRenderTarget(Avalonia.PixelSize,System.Double) baseline/Avalonia/lib/net8.0/Avalonia.Base.dll current/Avalonia/lib/net8.0/Avalonia.Base.dll CP0002 - M:Avalonia.Media.RadialGradientBrush.set_Radius(System.Double) + M:Avalonia.Platform.IPlatformRenderInterfaceContext.CreateRenderTarget(System.Collections.Generic.IEnumerable{System.Object}) baseline/Avalonia/lib/net8.0/Avalonia.Base.dll current/Avalonia/lib/net8.0/Avalonia.Base.dll CP0002 - M:Avalonia.Media.StreamGeometryContext.ArcTo(Avalonia.Point,Avalonia.Size,System.Double,System.Boolean,Avalonia.Media.SweepDirection) + M:Avalonia.Platform.IRenderTarget.CreateDrawingContext(Avalonia.PixelSize,Avalonia.Platform.RenderTargetDrawingContextProperties@) baseline/Avalonia/lib/net8.0/Avalonia.Base.dll current/Avalonia/lib/net8.0/Avalonia.Base.dll CP0002 - M:Avalonia.Media.StreamGeometryContext.CubicBezierTo(Avalonia.Point,Avalonia.Point,Avalonia.Point) + M:Avalonia.Platform.IRenderTarget.CreateDrawingContext(System.Boolean) baseline/Avalonia/lib/net8.0/Avalonia.Base.dll current/Avalonia/lib/net8.0/Avalonia.Base.dll CP0002 - M:Avalonia.Media.StreamGeometryContext.LineTo(Avalonia.Point) + M:Avalonia.Platform.LockedFramebuffer.#ctor(System.IntPtr,Avalonia.PixelSize,System.Int32,Avalonia.Vector,Avalonia.Platform.PixelFormat,System.Action) baseline/Avalonia/lib/net8.0/Avalonia.Base.dll current/Avalonia/lib/net8.0/Avalonia.Base.dll CP0002 - M:Avalonia.Media.StreamGeometryContext.QuadraticBezierTo(Avalonia.Point,Avalonia.Point) + M:Avalonia.Rendering.Composition.ICompositionGpuImportedObject.get_ImportCompeted baseline/Avalonia/lib/net8.0/Avalonia.Base.dll current/Avalonia/lib/net8.0/Avalonia.Base.dll CP0002 - M:Avalonia.Media.TextFormatting.GenericTextRunProperties.#ctor(Avalonia.Media.Typeface,Avalonia.Media.FontFeatureCollection,System.Double,Avalonia.Media.TextDecorationCollection,Avalonia.Media.IBrush,Avalonia.Media.IBrush,Avalonia.Media.BaselineAlignment,System.Globalization.CultureInfo) + M:Avalonia.Rendering.DefaultRenderTimer.add_Tick(System.Action{System.TimeSpan}) baseline/Avalonia/lib/net8.0/Avalonia.Base.dll current/Avalonia/lib/net8.0/Avalonia.Base.dll CP0002 - M:Avalonia.Media.TextFormatting.GenericTextRunProperties.#ctor(Avalonia.Media.Typeface,System.Double,Avalonia.Media.TextDecorationCollection,Avalonia.Media.IBrush,Avalonia.Media.IBrush,Avalonia.Media.BaselineAlignment,System.Globalization.CultureInfo) + M:Avalonia.Rendering.DefaultRenderTimer.remove_Tick(System.Action{System.TimeSpan}) baseline/Avalonia/lib/net8.0/Avalonia.Base.dll current/Avalonia/lib/net8.0/Avalonia.Base.dll CP0002 - M:Avalonia.Media.TextFormatting.TextCollapsingProperties.CreateCollapsedRuns(Avalonia.Media.TextFormatting.TextLine,System.Int32,Avalonia.Media.FlowDirection,Avalonia.Media.TextFormatting.TextRun) + M:Avalonia.Rendering.DefaultRenderTimer.Start baseline/Avalonia/lib/net8.0/Avalonia.Base.dll current/Avalonia/lib/net8.0/Avalonia.Base.dll CP0002 - M:Avalonia.Media.TextFormatting.TextLayout.#ctor(System.String,Avalonia.Media.Typeface,Avalonia.Media.FontFeatureCollection,System.Double,Avalonia.Media.IBrush,Avalonia.Media.TextAlignment,Avalonia.Media.TextWrapping,Avalonia.Media.TextTrimming,Avalonia.Media.TextDecorationCollection,Avalonia.Media.FlowDirection,System.Double,System.Double,System.Double,System.Double,System.Int32,System.Collections.Generic.IReadOnlyList{Avalonia.Utilities.ValueSpan{Avalonia.Media.TextFormatting.TextRunProperties}}) + M:Avalonia.Rendering.DefaultRenderTimer.Stop baseline/Avalonia/lib/net8.0/Avalonia.Base.dll current/Avalonia/lib/net8.0/Avalonia.Base.dll CP0002 - M:Avalonia.Media.TextFormatting.TextLayout.#ctor(System.String,Avalonia.Media.Typeface,System.Double,Avalonia.Media.IBrush,Avalonia.Media.TextAlignment,Avalonia.Media.TextWrapping,Avalonia.Media.TextTrimming,Avalonia.Media.TextDecorationCollection,Avalonia.Media.FlowDirection,System.Double,System.Double,System.Double,System.Double,System.Int32,System.Collections.Generic.IReadOnlyList{Avalonia.Utilities.ValueSpan{Avalonia.Media.TextFormatting.TextRunProperties}}) + M:Avalonia.Rendering.IRenderTimer.add_Tick(System.Action{System.TimeSpan}) baseline/Avalonia/lib/net8.0/Avalonia.Base.dll current/Avalonia/lib/net8.0/Avalonia.Base.dll CP0002 - M:Avalonia.Media.TextFormatting.TextShaperOptions.#ctor(Avalonia.Media.GlyphTypeface,System.Collections.Generic.IReadOnlyList{Avalonia.Media.FontFeature},System.Double,System.SByte,System.Globalization.CultureInfo,System.Double,System.Double) + M:Avalonia.Rendering.IRenderTimer.remove_Tick(System.Action{System.TimeSpan}) baseline/Avalonia/lib/net8.0/Avalonia.Base.dll current/Avalonia/lib/net8.0/Avalonia.Base.dll CP0002 - M:Avalonia.Media.TextFormatting.TextShaperOptions.#ctor(Avalonia.Media.GlyphTypeface,System.Double,System.SByte,System.Globalization.CultureInfo,System.Double,System.Double) + M:Avalonia.Rendering.SceneInvalidatedEventArgs.#ctor(Avalonia.Rendering.IRenderRoot,Avalonia.Rect) baseline/Avalonia/lib/net8.0/Avalonia.Base.dll current/Avalonia/lib/net8.0/Avalonia.Base.dll CP0002 - M:Avalonia.Platform.IDrawingContextImplWithEffects.PushEffect(Avalonia.Media.IEffect) + M:Avalonia.Rendering.SceneInvalidatedEventArgs.get_RenderRoot baseline/Avalonia/lib/net8.0/Avalonia.Base.dll current/Avalonia/lib/net8.0/Avalonia.Base.dll CP0002 - M:Avalonia.Platform.IFontManagerImpl.TryMatchCharacter(System.Int32,Avalonia.Media.FontStyle,Avalonia.Media.FontWeight,Avalonia.Media.FontStretch,System.Globalization.CultureInfo,Avalonia.Media.Typeface@) + M:Avalonia.Rendering.SleepLoopRenderTimer.add_Tick(System.Action{System.TimeSpan}) baseline/Avalonia/lib/net8.0/Avalonia.Base.dll current/Avalonia/lib/net8.0/Avalonia.Base.dll CP0002 - M:Avalonia.Platform.IGeometryContext.ArcTo(Avalonia.Point,Avalonia.Size,System.Double,System.Boolean,Avalonia.Media.SweepDirection) + M:Avalonia.Rendering.SleepLoopRenderTimer.remove_Tick(System.Action{System.TimeSpan}) baseline/Avalonia/lib/net8.0/Avalonia.Base.dll current/Avalonia/lib/net8.0/Avalonia.Base.dll CP0002 - M:Avalonia.Platform.IGeometryContext.CubicBezierTo(Avalonia.Point,Avalonia.Point,Avalonia.Point) + M:Avalonia.Rendering.ThreadProxyRenderTimer.add_Tick(System.Action{System.TimeSpan}) baseline/Avalonia/lib/net8.0/Avalonia.Base.dll current/Avalonia/lib/net8.0/Avalonia.Base.dll CP0002 - M:Avalonia.Platform.IGeometryContext.LineTo(Avalonia.Point) + M:Avalonia.Rendering.ThreadProxyRenderTimer.remove_Tick(System.Action{System.TimeSpan}) baseline/Avalonia/lib/net8.0/Avalonia.Base.dll current/Avalonia/lib/net8.0/Avalonia.Base.dll CP0002 - M:Avalonia.Platform.IGeometryContext.QuadraticBezierTo(Avalonia.Point,Avalonia.Point) + M:Avalonia.Utilities.AvaloniaResourcesIndexReaderWriter.WriteResources(System.IO.Stream,System.Collections.Generic.List{System.ValueTuple{System.String,System.Int32,System.Func{System.IO.Stream}}}) baseline/Avalonia/lib/net8.0/Avalonia.Base.dll current/Avalonia/lib/net8.0/Avalonia.Base.dll CP0002 - M:Avalonia.Platform.IPlatformRenderInterfaceContext.CreateOffscreenRenderTarget(Avalonia.PixelSize,System.Double) + M:Avalonia.Visual.get_VisualRoot baseline/Avalonia/lib/net8.0/Avalonia.Base.dll current/Avalonia/lib/net8.0/Avalonia.Base.dll CP0002 - M:Avalonia.Platform.LockedFramebuffer.#ctor(System.IntPtr,Avalonia.PixelSize,System.Int32,Avalonia.Vector,Avalonia.Platform.PixelFormat,System.Action) + M:Avalonia.Visuals.Platform.PathGeometryContext.ArcTo(Avalonia.Point,Avalonia.Size,System.Double,System.Boolean,Avalonia.Media.SweepDirection) baseline/Avalonia/lib/net8.0/Avalonia.Base.dll current/Avalonia/lib/net8.0/Avalonia.Base.dll CP0002 - M:Avalonia.Rendering.Composition.ICompositionGpuImportedObject.get_ImportCompeted + M:Avalonia.Visuals.Platform.PathGeometryContext.CubicBezierTo(Avalonia.Point,Avalonia.Point,Avalonia.Point) baseline/Avalonia/lib/net8.0/Avalonia.Base.dll current/Avalonia/lib/net8.0/Avalonia.Base.dll CP0002 - M:Avalonia.Utilities.AvaloniaResourcesIndexReaderWriter.WriteResources(System.IO.Stream,System.Collections.Generic.List{System.ValueTuple{System.String,System.Int32,System.Func{System.IO.Stream}}}) + M:Avalonia.Visuals.Platform.PathGeometryContext.LineTo(Avalonia.Point) baseline/Avalonia/lib/net8.0/Avalonia.Base.dll current/Avalonia/lib/net8.0/Avalonia.Base.dll CP0002 - M:Avalonia.Visuals.Platform.PathGeometryContext.ArcTo(Avalonia.Point,Avalonia.Size,System.Double,System.Boolean,Avalonia.Media.SweepDirection) + M:Avalonia.Visuals.Platform.PathGeometryContext.QuadraticBezierTo(Avalonia.Point,Avalonia.Point) baseline/Avalonia/lib/net8.0/Avalonia.Base.dll current/Avalonia/lib/net8.0/Avalonia.Base.dll CP0002 - M:Avalonia.Visuals.Platform.PathGeometryContext.CubicBezierTo(Avalonia.Point,Avalonia.Point,Avalonia.Point) + M:Avalonia.VisualTree.VisualExtensions.GetVisualRoot(Avalonia.Visual) baseline/Avalonia/lib/net8.0/Avalonia.Base.dll current/Avalonia/lib/net8.0/Avalonia.Base.dll CP0002 - M:Avalonia.Visuals.Platform.PathGeometryContext.LineTo(Avalonia.Point) + M:Avalonia.VisualTreeAttachmentEventArgs.#ctor(Avalonia.Visual,Avalonia.Rendering.IRenderRoot) baseline/Avalonia/lib/net8.0/Avalonia.Base.dll current/Avalonia/lib/net8.0/Avalonia.Base.dll CP0002 - M:Avalonia.Visuals.Platform.PathGeometryContext.QuadraticBezierTo(Avalonia.Point,Avalonia.Point) + M:Avalonia.VisualTreeAttachmentEventArgs.get_Root baseline/Avalonia/lib/net8.0/Avalonia.Base.dll current/Avalonia/lib/net8.0/Avalonia.Base.dll @@ -1927,12 +3499,24 @@ baseline/Avalonia/lib/net8.0/Avalonia.Controls.dll current/Avalonia/lib/net8.0/Avalonia.Controls.dll + + CP0002 + F:Avalonia.Controls.Control.ContextRequestedEvent + baseline/Avalonia/lib/net8.0/Avalonia.Controls.dll + current/Avalonia/lib/net8.0/Avalonia.Controls.dll + CP0002 F:Avalonia.Controls.Documents.Inline.TextDecorationsProperty baseline/Avalonia/lib/net8.0/Avalonia.Controls.dll current/Avalonia/lib/net8.0/Avalonia.Controls.dll + + CP0002 + F:Avalonia.Controls.DrawerPage.DrawerBreakpointWidthProperty + baseline/Avalonia/lib/net8.0/Avalonia.Controls.dll + current/Avalonia/lib/net8.0/Avalonia.Controls.dll + CP0002 F:Avalonia.Controls.NativeMenuBar.EnableMenuItemClickForwardingProperty @@ -1945,6 +3529,12 @@ baseline/Avalonia/lib/net8.0/Avalonia.Controls.dll current/Avalonia/lib/net8.0/Avalonia.Controls.dll + + CP0002 + F:Avalonia.Controls.Primitives.FlyoutBase.IsOpenProperty + baseline/Avalonia/lib/net8.0/Avalonia.Controls.dll + current/Avalonia/lib/net8.0/Avalonia.Controls.dll + CP0002 F:Avalonia.Controls.Primitives.Popup.PlacementModeProperty @@ -1969,6 +3559,18 @@ baseline/Avalonia/lib/net8.0/Avalonia.Controls.dll current/Avalonia/lib/net8.0/Avalonia.Controls.dll + + CP0002 + F:Avalonia.Controls.Primitives.VisualLayerManager.ChromeOverlayLayerProperty + baseline/Avalonia/lib/net8.0/Avalonia.Controls.dll + current/Avalonia/lib/net8.0/Avalonia.Controls.dll + + + CP0002 + F:Avalonia.Controls.TabItem.IconProperty + baseline/Avalonia/lib/net8.0/Avalonia.Controls.dll + current/Avalonia/lib/net8.0/Avalonia.Controls.dll + CP0002 F:Avalonia.Controls.TextBlock.LetterSpacingProperty @@ -1981,6 +3583,24 @@ baseline/Avalonia/lib/net8.0/Avalonia.Controls.dll current/Avalonia/lib/net8.0/Avalonia.Controls.dll + + CP0002 + F:Avalonia.Controls.TopLevel.PointerOverElementProperty + baseline/Avalonia/lib/net8.0/Avalonia.Controls.dll + current/Avalonia/lib/net8.0/Avalonia.Controls.dll + + + CP0002 + F:Avalonia.Controls.Window.ExtendClientAreaChromeHintsProperty + baseline/Avalonia/lib/net8.0/Avalonia.Controls.dll + current/Avalonia/lib/net8.0/Avalonia.Controls.dll + + + CP0002 + F:Avalonia.Controls.Window.SystemDecorationsProperty + baseline/Avalonia/lib/net8.0/Avalonia.Controls.dll + current/Avalonia/lib/net8.0/Avalonia.Controls.dll + CP0002 M:Avalonia.AppBuilder.get_LifetimeOverride @@ -1999,6 +3619,12 @@ baseline/Avalonia/lib/net8.0/Avalonia.Controls.dll current/Avalonia/lib/net8.0/Avalonia.Controls.dll + + CP0002 + M:Avalonia.Automation.Peers.AutomationPeer.GetVisualRootCore + baseline/Avalonia/lib/net8.0/Avalonia.Controls.dll + current/Avalonia/lib/net8.0/Avalonia.Controls.dll + CP0002 M:Avalonia.Controls.ContextMenu.get_PlacementMode @@ -2083,6 +3709,24 @@ baseline/Avalonia/lib/net8.0/Avalonia.Controls.dll current/Avalonia/lib/net8.0/Avalonia.Controls.dll + + CP0002 + M:Avalonia.Controls.DrawerPage.get_DrawerBreakpointWidth + baseline/Avalonia/lib/net8.0/Avalonia.Controls.dll + current/Avalonia/lib/net8.0/Avalonia.Controls.dll + + + CP0002 + M:Avalonia.Controls.DrawerPage.set_DrawerBreakpointWidth(System.Double) + baseline/Avalonia/lib/net8.0/Avalonia.Controls.dll + current/Avalonia/lib/net8.0/Avalonia.Controls.dll + + + CP0002 + M:Avalonia.Controls.Embedding.Offscreen.OffscreenTopLevelImplBase.get_Surfaces + baseline/Avalonia/lib/net8.0/Avalonia.Controls.dll + current/Avalonia/lib/net8.0/Avalonia.Controls.dll + CP0002 M:Avalonia.Controls.Generators.ItemContainerGenerator.ContainerFromIndex(System.Int32) @@ -2113,6 +3757,18 @@ baseline/Avalonia/lib/net8.0/Avalonia.Controls.dll current/Avalonia/lib/net8.0/Avalonia.Controls.dll + + CP0002 + M:Avalonia.Controls.PageSelectionChangedEventArgs.#ctor(Avalonia.Controls.Page,Avalonia.Controls.Page) + baseline/Avalonia/lib/net8.0/Avalonia.Controls.dll + current/Avalonia/lib/net8.0/Avalonia.Controls.dll + + + CP0002 + M:Avalonia.Controls.Platform.DefaultMenuInteractionHandler.GotFocus(System.Object,Avalonia.Input.GotFocusEventArgs) + baseline/Avalonia/lib/net8.0/Avalonia.Controls.dll + current/Avalonia/lib/net8.0/Avalonia.Controls.dll + CP0002 M:Avalonia.Controls.Platform.IInsetsManager.get_DisplayEdgeToEdge @@ -2143,18 +3799,54 @@ baseline/Avalonia/lib/net8.0/Avalonia.Controls.dll current/Avalonia/lib/net8.0/Avalonia.Controls.dll + + CP0002 + M:Avalonia.Controls.Primitives.AdornerLayer.#ctor + baseline/Avalonia/lib/net8.0/Avalonia.Controls.dll + current/Avalonia/lib/net8.0/Avalonia.Controls.dll + + + CP0002 + M:Avalonia.Controls.Primitives.OverlayPopupHost.#ctor(Avalonia.Controls.Primitives.OverlayLayer) + baseline/Avalonia/lib/net8.0/Avalonia.Controls.dll + current/Avalonia/lib/net8.0/Avalonia.Controls.dll + CP0002 M:Avalonia.Controls.Primitives.OverlayPopupHost.ConfigurePosition(Avalonia.Visual,Avalonia.Controls.PlacementMode,Avalonia.Point,Avalonia.Controls.Primitives.PopupPositioning.PopupAnchor,Avalonia.Controls.Primitives.PopupPositioning.PopupGravity,Avalonia.Controls.Primitives.PopupPositioning.PopupPositionerConstraintAdjustment,System.Nullable{Avalonia.Rect}) baseline/Avalonia/lib/net8.0/Avalonia.Controls.dll current/Avalonia/lib/net8.0/Avalonia.Controls.dll + + CP0002 + M:Avalonia.Controls.Primitives.OverlayPopupHost.CreatePopupHost(Avalonia.Visual,Avalonia.IAvaloniaDependencyResolver,System.Boolean) + baseline/Avalonia/lib/net8.0/Avalonia.Controls.dll + current/Avalonia/lib/net8.0/Avalonia.Controls.dll + CP0002 M:Avalonia.Controls.Primitives.OverlayPopupHost.CreatePopupHost(Avalonia.Visual,Avalonia.IAvaloniaDependencyResolver) baseline/Avalonia/lib/net8.0/Avalonia.Controls.dll current/Avalonia/lib/net8.0/Avalonia.Controls.dll + + CP0002 + M:Avalonia.Controls.Primitives.OverlayPopupHost.SetChild(Avalonia.Controls.Control) + baseline/Avalonia/lib/net8.0/Avalonia.Controls.dll + current/Avalonia/lib/net8.0/Avalonia.Controls.dll + + + CP0002 + M:Avalonia.Controls.Primitives.OverlayPopupHost.TakeFocus + baseline/Avalonia/lib/net8.0/Avalonia.Controls.dll + current/Avalonia/lib/net8.0/Avalonia.Controls.dll + + + CP0002 + M:Avalonia.Controls.Primitives.Popup.get_Host + baseline/Avalonia/lib/net8.0/Avalonia.Controls.dll + current/Avalonia/lib/net8.0/Avalonia.Controls.dll + CP0002 M:Avalonia.Controls.Primitives.Popup.get_PlacementMode @@ -2169,103 +3861,247 @@ CP0002 - M:Avalonia.Controls.Primitives.PopupRoot.ConfigurePosition(Avalonia.Visual,Avalonia.Controls.PlacementMode,Avalonia.Point,Avalonia.Controls.Primitives.PopupPositioning.PopupAnchor,Avalonia.Controls.Primitives.PopupPositioning.PopupGravity,Avalonia.Controls.Primitives.PopupPositioning.PopupPositionerConstraintAdjustment,System.Nullable{Avalonia.Rect}) + M:Avalonia.Controls.Primitives.PopupRoot.ConfigurePosition(Avalonia.Visual,Avalonia.Controls.PlacementMode,Avalonia.Point,Avalonia.Controls.Primitives.PopupPositioning.PopupAnchor,Avalonia.Controls.Primitives.PopupPositioning.PopupGravity,Avalonia.Controls.Primitives.PopupPositioning.PopupPositionerConstraintAdjustment,System.Nullable{Avalonia.Rect}) + baseline/Avalonia/lib/net8.0/Avalonia.Controls.dll + current/Avalonia/lib/net8.0/Avalonia.Controls.dll + + + CP0002 + M:Avalonia.Controls.Primitives.TextSearch.GetText(Avalonia.Controls.Control) + baseline/Avalonia/lib/net8.0/Avalonia.Controls.dll + current/Avalonia/lib/net8.0/Avalonia.Controls.dll + + + CP0002 + M:Avalonia.Controls.Primitives.TextSearch.GetText(Avalonia.Interactivity.Interactive) + baseline/Avalonia/lib/net8.0/Avalonia.Controls.dll + current/Avalonia/lib/net8.0/Avalonia.Controls.dll + + + CP0002 + M:Avalonia.Controls.Primitives.TextSearch.GetTextBinding(Avalonia.Interactivity.Interactive) + baseline/Avalonia/lib/net8.0/Avalonia.Controls.dll + current/Avalonia/lib/net8.0/Avalonia.Controls.dll + + + CP0002 + M:Avalonia.Controls.Primitives.TextSearch.SetText(Avalonia.Controls.Control,System.String) + baseline/Avalonia/lib/net8.0/Avalonia.Controls.dll + current/Avalonia/lib/net8.0/Avalonia.Controls.dll + + + CP0002 + M:Avalonia.Controls.Primitives.TextSearch.SetText(Avalonia.Interactivity.Interactive,System.String) + baseline/Avalonia/lib/net8.0/Avalonia.Controls.dll + current/Avalonia/lib/net8.0/Avalonia.Controls.dll + + + CP0002 + M:Avalonia.Controls.Primitives.TextSearch.SetTextBinding(Avalonia.Interactivity.Interactive,Avalonia.Data.BindingBase) + baseline/Avalonia/lib/net8.0/Avalonia.Controls.dll + current/Avalonia/lib/net8.0/Avalonia.Controls.dll + + + CP0002 + M:Avalonia.Controls.Primitives.ToggleButton.add_Checked(System.EventHandler{Avalonia.Interactivity.RoutedEventArgs}) + baseline/Avalonia/lib/net8.0/Avalonia.Controls.dll + current/Avalonia/lib/net8.0/Avalonia.Controls.dll + + + CP0002 + M:Avalonia.Controls.Primitives.ToggleButton.add_Indeterminate(System.EventHandler{Avalonia.Interactivity.RoutedEventArgs}) + baseline/Avalonia/lib/net8.0/Avalonia.Controls.dll + current/Avalonia/lib/net8.0/Avalonia.Controls.dll + + + CP0002 + M:Avalonia.Controls.Primitives.ToggleButton.add_Unchecked(System.EventHandler{Avalonia.Interactivity.RoutedEventArgs}) + baseline/Avalonia/lib/net8.0/Avalonia.Controls.dll + current/Avalonia/lib/net8.0/Avalonia.Controls.dll + + + CP0002 + M:Avalonia.Controls.Primitives.ToggleButton.OnChecked(Avalonia.Interactivity.RoutedEventArgs) + baseline/Avalonia/lib/net8.0/Avalonia.Controls.dll + current/Avalonia/lib/net8.0/Avalonia.Controls.dll + + + CP0002 + M:Avalonia.Controls.Primitives.ToggleButton.OnIndeterminate(Avalonia.Interactivity.RoutedEventArgs) + baseline/Avalonia/lib/net8.0/Avalonia.Controls.dll + current/Avalonia/lib/net8.0/Avalonia.Controls.dll + + + CP0002 + M:Avalonia.Controls.Primitives.ToggleButton.OnUnchecked(Avalonia.Interactivity.RoutedEventArgs) + baseline/Avalonia/lib/net8.0/Avalonia.Controls.dll + current/Avalonia/lib/net8.0/Avalonia.Controls.dll + + + CP0002 + M:Avalonia.Controls.Primitives.ToggleButton.remove_Checked(System.EventHandler{Avalonia.Interactivity.RoutedEventArgs}) + baseline/Avalonia/lib/net8.0/Avalonia.Controls.dll + current/Avalonia/lib/net8.0/Avalonia.Controls.dll + + + CP0002 + M:Avalonia.Controls.Primitives.ToggleButton.remove_Indeterminate(System.EventHandler{Avalonia.Interactivity.RoutedEventArgs}) + baseline/Avalonia/lib/net8.0/Avalonia.Controls.dll + current/Avalonia/lib/net8.0/Avalonia.Controls.dll + + + CP0002 + M:Avalonia.Controls.Primitives.ToggleButton.remove_Unchecked(System.EventHandler{Avalonia.Interactivity.RoutedEventArgs}) + baseline/Avalonia/lib/net8.0/Avalonia.Controls.dll + current/Avalonia/lib/net8.0/Avalonia.Controls.dll + + + CP0002 + M:Avalonia.Controls.Primitives.VisualLayerManager.get_AdornerLayer + baseline/Avalonia/lib/net8.0/Avalonia.Controls.dll + current/Avalonia/lib/net8.0/Avalonia.Controls.dll + + + CP0002 + M:Avalonia.Controls.Primitives.VisualLayerManager.get_ChromeOverlayLayer + baseline/Avalonia/lib/net8.0/Avalonia.Controls.dll + current/Avalonia/lib/net8.0/Avalonia.Controls.dll + + + CP0002 + M:Avalonia.Controls.Primitives.VisualLayerManager.get_IsPopup + baseline/Avalonia/lib/net8.0/Avalonia.Controls.dll + current/Avalonia/lib/net8.0/Avalonia.Controls.dll + + + CP0002 + M:Avalonia.Controls.Primitives.VisualLayerManager.get_LightDismissOverlayLayer + baseline/Avalonia/lib/net8.0/Avalonia.Controls.dll + current/Avalonia/lib/net8.0/Avalonia.Controls.dll + + + CP0002 + M:Avalonia.Controls.Primitives.VisualLayerManager.get_OverlayLayer + baseline/Avalonia/lib/net8.0/Avalonia.Controls.dll + current/Avalonia/lib/net8.0/Avalonia.Controls.dll + + + CP0002 + M:Avalonia.Controls.Primitives.VisualLayerManager.get_TextSelectorLayer + baseline/Avalonia/lib/net8.0/Avalonia.Controls.dll + current/Avalonia/lib/net8.0/Avalonia.Controls.dll + + + CP0002 + M:Avalonia.Controls.Primitives.VisualLayerManager.set_IsPopup(System.Boolean) + baseline/Avalonia/lib/net8.0/Avalonia.Controls.dll + current/Avalonia/lib/net8.0/Avalonia.Controls.dll + + + CP0002 + M:Avalonia.Controls.Screens.ScreenFromWindow(Avalonia.Platform.IWindowBaseImpl) + baseline/Avalonia/lib/net8.0/Avalonia.Controls.dll + current/Avalonia/lib/net8.0/Avalonia.Controls.dll + + + CP0002 + M:Avalonia.Controls.TabbedPage.FindNextEnabledTab(System.Int32,System.Int32) baseline/Avalonia/lib/net8.0/Avalonia.Controls.dll current/Avalonia/lib/net8.0/Avalonia.Controls.dll CP0002 - M:Avalonia.Controls.Primitives.TextSearch.GetText(Avalonia.Controls.Control) + M:Avalonia.Controls.TabItem.get_Icon baseline/Avalonia/lib/net8.0/Avalonia.Controls.dll current/Avalonia/lib/net8.0/Avalonia.Controls.dll CP0002 - M:Avalonia.Controls.Primitives.TextSearch.SetText(Avalonia.Controls.Control,System.String) + M:Avalonia.Controls.TabItem.SubscribeToOwnerProperties(Avalonia.AvaloniaObject) baseline/Avalonia/lib/net8.0/Avalonia.Controls.dll current/Avalonia/lib/net8.0/Avalonia.Controls.dll CP0002 - M:Avalonia.Controls.Primitives.ToggleButton.add_Checked(System.EventHandler{Avalonia.Interactivity.RoutedEventArgs}) + M:Avalonia.Controls.TopLevel.#ctor(Avalonia.Platform.ITopLevelImpl,Avalonia.IAvaloniaDependencyResolver) baseline/Avalonia/lib/net8.0/Avalonia.Controls.dll current/Avalonia/lib/net8.0/Avalonia.Controls.dll CP0002 - M:Avalonia.Controls.Primitives.ToggleButton.add_Indeterminate(System.EventHandler{Avalonia.Interactivity.RoutedEventArgs}) + M:Avalonia.Controls.TopLevel.get_PlatformSettings baseline/Avalonia/lib/net8.0/Avalonia.Controls.dll current/Avalonia/lib/net8.0/Avalonia.Controls.dll CP0002 - M:Avalonia.Controls.Primitives.ToggleButton.add_Unchecked(System.EventHandler{Avalonia.Interactivity.RoutedEventArgs}) + M:Avalonia.Controls.TopLevel.StartRendering baseline/Avalonia/lib/net8.0/Avalonia.Controls.dll current/Avalonia/lib/net8.0/Avalonia.Controls.dll CP0002 - M:Avalonia.Controls.Primitives.ToggleButton.OnChecked(Avalonia.Interactivity.RoutedEventArgs) + M:Avalonia.Controls.TopLevel.StopRendering baseline/Avalonia/lib/net8.0/Avalonia.Controls.dll current/Avalonia/lib/net8.0/Avalonia.Controls.dll CP0002 - M:Avalonia.Controls.Primitives.ToggleButton.OnIndeterminate(Avalonia.Interactivity.RoutedEventArgs) + M:Avalonia.Controls.TreeView.get_ItemContainerGenerator baseline/Avalonia/lib/net8.0/Avalonia.Controls.dll current/Avalonia/lib/net8.0/Avalonia.Controls.dll CP0002 - M:Avalonia.Controls.Primitives.ToggleButton.OnUnchecked(Avalonia.Interactivity.RoutedEventArgs) + M:Avalonia.Controls.Window.get_ExtendClientAreaChromeHints baseline/Avalonia/lib/net8.0/Avalonia.Controls.dll current/Avalonia/lib/net8.0/Avalonia.Controls.dll CP0002 - M:Avalonia.Controls.Primitives.ToggleButton.remove_Checked(System.EventHandler{Avalonia.Interactivity.RoutedEventArgs}) + M:Avalonia.Controls.Window.get_SystemDecorations baseline/Avalonia/lib/net8.0/Avalonia.Controls.dll current/Avalonia/lib/net8.0/Avalonia.Controls.dll CP0002 - M:Avalonia.Controls.Primitives.ToggleButton.remove_Indeterminate(System.EventHandler{Avalonia.Interactivity.RoutedEventArgs}) + M:Avalonia.Controls.Window.set_ExtendClientAreaChromeHints(Avalonia.Platform.ExtendClientAreaChromeHints) baseline/Avalonia/lib/net8.0/Avalonia.Controls.dll current/Avalonia/lib/net8.0/Avalonia.Controls.dll CP0002 - M:Avalonia.Controls.Primitives.ToggleButton.remove_Unchecked(System.EventHandler{Avalonia.Interactivity.RoutedEventArgs}) + M:Avalonia.Controls.Window.SortWindowsByZOrder(Avalonia.Controls.Window[]) baseline/Avalonia/lib/net8.0/Avalonia.Controls.dll current/Avalonia/lib/net8.0/Avalonia.Controls.dll CP0002 - M:Avalonia.Controls.Screens.ScreenFromWindow(Avalonia.Platform.IWindowBaseImpl) + M:Avalonia.Controls.WindowBase.ArrangeSetBounds(Avalonia.Size) baseline/Avalonia/lib/net8.0/Avalonia.Controls.dll current/Avalonia/lib/net8.0/Avalonia.Controls.dll CP0002 - M:Avalonia.Controls.TabItem.SubscribeToOwnerProperties(Avalonia.AvaloniaObject) + M:Avalonia.Platform.ITopLevelImpl.get_Surfaces baseline/Avalonia/lib/net8.0/Avalonia.Controls.dll current/Avalonia/lib/net8.0/Avalonia.Controls.dll CP0002 - M:Avalonia.Controls.TreeView.get_ItemContainerGenerator + M:Avalonia.Platform.IWindowImpl.GetWindowsZOrder(System.Span{Avalonia.Controls.Window},System.Span{System.Int64}) baseline/Avalonia/lib/net8.0/Avalonia.Controls.dll current/Avalonia/lib/net8.0/Avalonia.Controls.dll CP0002 - M:Avalonia.Controls.Window.SortWindowsByZOrder(Avalonia.Controls.Window[]) + M:Avalonia.Platform.IWindowImpl.SetExtendClientAreaChromeHints(Avalonia.Platform.ExtendClientAreaChromeHints) baseline/Avalonia/lib/net8.0/Avalonia.Controls.dll current/Avalonia/lib/net8.0/Avalonia.Controls.dll CP0002 - M:Avalonia.Platform.IWindowImpl.GetWindowsZOrder(System.Span{Avalonia.Controls.Window},System.Span{System.Int64}) + M:Avalonia.Platform.IWindowImpl.SetSystemDecorations(Avalonia.Controls.SystemDecorations) baseline/Avalonia/lib/net8.0/Avalonia.Controls.dll current/Avalonia/lib/net8.0/Avalonia.Controls.dll @@ -2365,18 +4201,66 @@ baseline/Avalonia/lib/net8.0/Avalonia.OpenGL.dll current/Avalonia/lib/net8.0/Avalonia.OpenGL.dll + + CP0002 + M:Avalonia.OpenGL.Egl.EglPlatformSurfaceRenderTargetBase.BeginDraw(System.Nullable{Avalonia.PixelSize}) + baseline/Avalonia/lib/net8.0/Avalonia.OpenGL.dll + current/Avalonia/lib/net8.0/Avalonia.OpenGL.dll + CP0002 M:Avalonia.OpenGL.Egl.EglPlatformSurfaceRenderTargetBase.BeginDrawCore baseline/Avalonia/lib/net8.0/Avalonia.OpenGL.dll current/Avalonia/lib/net8.0/Avalonia.OpenGL.dll + + CP0002 + M:Avalonia.OpenGL.Egl.EglPlatformSurfaceRenderTargetBase.BeginDrawCore(System.Nullable{Avalonia.PixelSize}) + baseline/Avalonia/lib/net8.0/Avalonia.OpenGL.dll + current/Avalonia/lib/net8.0/Avalonia.OpenGL.dll + + + CP0002 + M:Avalonia.OpenGL.IGlPlatformSurfaceRenderTargetFactory.CanRenderToSurface(Avalonia.OpenGL.IGlContext,System.Object) + baseline/Avalonia/lib/net8.0/Avalonia.OpenGL.dll + current/Avalonia/lib/net8.0/Avalonia.OpenGL.dll + + + CP0002 + M:Avalonia.OpenGL.IGlPlatformSurfaceRenderTargetFactory.CreateRenderTarget(Avalonia.OpenGL.IGlContext,System.Object) + baseline/Avalonia/lib/net8.0/Avalonia.OpenGL.dll + current/Avalonia/lib/net8.0/Avalonia.OpenGL.dll + CP0002 M:Avalonia.OpenGL.Surfaces.IGlPlatformSurfaceRenderTarget.BeginDraw baseline/Avalonia/lib/net8.0/Avalonia.OpenGL.dll current/Avalonia/lib/net8.0/Avalonia.OpenGL.dll + + CP0002 + M:Avalonia.OpenGL.Surfaces.IGlPlatformSurfaceRenderTarget.BeginDraw(System.Nullable{Avalonia.PixelSize}) + baseline/Avalonia/lib/net8.0/Avalonia.OpenGL.dll + current/Avalonia/lib/net8.0/Avalonia.OpenGL.dll + + + CP0002 + M:Avalonia.Vulkan.IVulkanKhrSurfacePlatformSurfaceFactory.CanRenderToSurface(Avalonia.Vulkan.IVulkanPlatformGraphicsContext,System.Object) + baseline/Avalonia/lib/net8.0/Avalonia.Vulkan.dll + current/Avalonia/lib/net8.0/Avalonia.Vulkan.dll + + + CP0002 + M:Avalonia.Vulkan.IVulkanKhrSurfacePlatformSurfaceFactory.CreateSurface(Avalonia.Vulkan.IVulkanPlatformGraphicsContext,System.Object) + baseline/Avalonia/lib/net8.0/Avalonia.Vulkan.dll + current/Avalonia/lib/net8.0/Avalonia.Vulkan.dll + + + CP0002 + M:Avalonia.Vulkan.IVulkanPlatformGraphicsContext.CreateRenderTarget(System.Collections.Generic.IEnumerable{System.Object}) + baseline/Avalonia/lib/net8.0/Avalonia.Vulkan.dll + current/Avalonia/lib/net8.0/Avalonia.Vulkan.dll + CP0002 F:Avalonia.Media.Fonts.FontCollectionBase._glyphTypefaceCache @@ -2419,24 +4303,108 @@ baseline/netstandard2.0/Avalonia.Base.dll target/netstandard2.0/Avalonia.Base.dll + + CP0005 + M:Avalonia.Controls.Embedding.Offscreen.OffscreenTopLevelImplBase.get_Surfaces + baseline/Avalonia/lib/net10.0/Avalonia.Controls.dll + current/Avalonia/lib/net10.0/Avalonia.Controls.dll + + + CP0005 + P:Avalonia.Controls.Embedding.Offscreen.OffscreenTopLevelImplBase.Surfaces + baseline/Avalonia/lib/net10.0/Avalonia.Controls.dll + current/Avalonia/lib/net10.0/Avalonia.Controls.dll + + + CP0005 + M:Avalonia.OpenGL.Egl.EglPlatformSurfaceRenderTargetBase.BeginDrawCore(Avalonia.Platform.IRenderTarget.RenderTargetSceneInfo) + baseline/Avalonia/lib/net10.0/Avalonia.OpenGL.dll + current/Avalonia/lib/net10.0/Avalonia.OpenGL.dll + CP0005 M:Avalonia.OpenGL.Egl.EglPlatformSurfaceRenderTargetBase.BeginDrawCore(System.Nullable{Avalonia.PixelSize}) baseline/Avalonia/lib/net10.0/Avalonia.OpenGL.dll current/Avalonia/lib/net10.0/Avalonia.OpenGL.dll + + CP0005 + M:Avalonia.Controls.Embedding.Offscreen.OffscreenTopLevelImplBase.get_Surfaces + baseline/Avalonia/lib/net8.0/Avalonia.Controls.dll + current/Avalonia/lib/net8.0/Avalonia.Controls.dll + + + CP0005 + P:Avalonia.Controls.Embedding.Offscreen.OffscreenTopLevelImplBase.Surfaces + baseline/Avalonia/lib/net8.0/Avalonia.Controls.dll + current/Avalonia/lib/net8.0/Avalonia.Controls.dll + + + CP0005 + M:Avalonia.OpenGL.Egl.EglPlatformSurfaceRenderTargetBase.BeginDrawCore(Avalonia.Platform.IRenderTarget.RenderTargetSceneInfo) + baseline/Avalonia/lib/net8.0/Avalonia.OpenGL.dll + current/Avalonia/lib/net8.0/Avalonia.OpenGL.dll + CP0005 M:Avalonia.OpenGL.Egl.EglPlatformSurfaceRenderTargetBase.BeginDrawCore(System.Nullable{Avalonia.PixelSize}) baseline/Avalonia/lib/net8.0/Avalonia.OpenGL.dll current/Avalonia/lib/net8.0/Avalonia.OpenGL.dll + + CP0006 + E:Avalonia.Input.IInputElement.GotFocus + baseline/Avalonia/lib/net10.0/Avalonia.Base.dll + current/Avalonia/lib/net10.0/Avalonia.Base.dll + + + CP0006 + E:Avalonia.Input.IInputElement.LostFocus + baseline/Avalonia/lib/net10.0/Avalonia.Base.dll + current/Avalonia/lib/net10.0/Avalonia.Base.dll + + + CP0006 + M:Avalonia.Input.IFocusManager.FindFirstFocusableElement + baseline/Avalonia/lib/net10.0/Avalonia.Base.dll + current/Avalonia/lib/net10.0/Avalonia.Base.dll + + + CP0006 + M:Avalonia.Input.IFocusManager.FindLastFocusableElement + baseline/Avalonia/lib/net10.0/Avalonia.Base.dll + current/Avalonia/lib/net10.0/Avalonia.Base.dll + + + CP0006 + M:Avalonia.Input.IFocusManager.FindNextElement(Avalonia.Input.NavigationDirection,Avalonia.Input.FindNextElementOptions) + baseline/Avalonia/lib/net10.0/Avalonia.Base.dll + current/Avalonia/lib/net10.0/Avalonia.Base.dll + + + CP0006 + M:Avalonia.Input.IFocusManager.Focus(Avalonia.Input.IInputElement,Avalonia.Input.NavigationMethod,Avalonia.Input.KeyModifiers) + baseline/Avalonia/lib/net10.0/Avalonia.Base.dll + current/Avalonia/lib/net10.0/Avalonia.Base.dll + + + CP0006 + M:Avalonia.Input.IFocusManager.TryMoveFocus(Avalonia.Input.NavigationDirection,Avalonia.Input.FindNextElementOptions) + baseline/Avalonia/lib/net10.0/Avalonia.Base.dll + current/Avalonia/lib/net10.0/Avalonia.Base.dll + CP0006 M:Avalonia.Input.IKeyboardNavigationHandler.Move(Avalonia.Input.IInputElement,Avalonia.Input.NavigationDirection,Avalonia.Input.KeyModifiers,System.Nullable{Avalonia.Input.KeyDeviceType}) baseline/Avalonia/lib/net10.0/Avalonia.Base.dll current/Avalonia/lib/net10.0/Avalonia.Base.dll + + CP0006 + M:Avalonia.Platform.ICursorFactory.CreateCursor(Avalonia.Media.Imaging.Bitmap,Avalonia.PixelPoint) + baseline/Avalonia/lib/net10.0/Avalonia.Base.dll + current/Avalonia/lib/net10.0/Avalonia.Base.dll + CP0006 M:Avalonia.Platform.IDrawingContextImpl.PopTextOptions @@ -2455,6 +4423,12 @@ baseline/Avalonia/lib/net10.0/Avalonia.Base.dll current/Avalonia/lib/net10.0/Avalonia.Base.dll + + CP0006 + M:Avalonia.Platform.IDrawingContextLayerImpl.CreateDrawingContext + baseline/Avalonia/lib/net10.0/Avalonia.Base.dll + current/Avalonia/lib/net10.0/Avalonia.Base.dll + CP0006 M:Avalonia.Platform.IFontManagerImpl.TryMatchCharacter(System.Int32,Avalonia.Media.FontStyle,Avalonia.Media.FontWeight,Avalonia.Media.FontStretch,System.String,System.Globalization.CultureInfo,Avalonia.Media.Typeface@) @@ -2491,12 +4465,54 @@ baseline/Avalonia/lib/net10.0/Avalonia.Base.dll current/Avalonia/lib/net10.0/Avalonia.Base.dll + + CP0006 + M:Avalonia.Platform.IPlatformRenderInterfaceContext.CreateRenderTarget(System.Collections.Generic.IEnumerable{Avalonia.Platform.Surfaces.IPlatformRenderSurface}) + baseline/Avalonia/lib/net10.0/Avalonia.Base.dll + current/Avalonia/lib/net10.0/Avalonia.Base.dll + CP0006 M:Avalonia.Platform.IRenderTarget.CreateDrawingContext(Avalonia.PixelSize,Avalonia.Platform.RenderTargetDrawingContextProperties@) baseline/Avalonia/lib/net10.0/Avalonia.Base.dll current/Avalonia/lib/net10.0/Avalonia.Base.dll + + CP0006 + M:Avalonia.Platform.IRenderTarget.CreateDrawingContext(Avalonia.Platform.IRenderTarget.RenderTargetSceneInfo,Avalonia.Platform.RenderTargetDrawingContextProperties@) + baseline/Avalonia/lib/net10.0/Avalonia.Base.dll + current/Avalonia/lib/net10.0/Avalonia.Base.dll + + + CP0006 + M:Avalonia.Platform.IRenderTargetBitmapImpl.CreateDrawingContext + baseline/Avalonia/lib/net10.0/Avalonia.Base.dll + current/Avalonia/lib/net10.0/Avalonia.Base.dll + + + CP0006 + M:Avalonia.Rendering.IRenderTimer.Start + baseline/Avalonia/lib/net10.0/Avalonia.Base.dll + current/Avalonia/lib/net10.0/Avalonia.Base.dll + + + CP0006 + M:Avalonia.Rendering.IRenderTimer.Stop + baseline/Avalonia/lib/net10.0/Avalonia.Base.dll + current/Avalonia/lib/net10.0/Avalonia.Base.dll + + + CP0006 + P:Avalonia.Input.IInputRoot.FocusRoot + baseline/Avalonia/lib/net10.0/Avalonia.Base.dll + current/Avalonia/lib/net10.0/Avalonia.Base.dll + + + CP0006 + P:Avalonia.Platform.IDrawingContextLayerImpl.IsCorrupted + baseline/Avalonia/lib/net10.0/Avalonia.Base.dll + current/Avalonia/lib/net10.0/Avalonia.Base.dll + CP0006 P:Avalonia.Platform.ILockedFramebuffer.AlphaFormat @@ -2521,12 +4537,48 @@ baseline/Avalonia/lib/net10.0/Avalonia.Base.dll current/Avalonia/lib/net10.0/Avalonia.Base.dll + + CP0006 + M:Avalonia.Platform.IWindowImpl.SetWindowDecorations(Avalonia.Controls.WindowDecorations) + baseline/Avalonia/lib/net10.0/Avalonia.Controls.dll + current/Avalonia/lib/net10.0/Avalonia.Controls.dll + CP0006 M:Avalonia.Platform.IWindowingPlatform.GetWindowsZOrder(System.ReadOnlySpan{Avalonia.Platform.IWindowImpl},System.Span{System.Int64}) baseline/Avalonia/lib/net10.0/Avalonia.Controls.dll current/Avalonia/lib/net10.0/Avalonia.Controls.dll + + CP0006 + P:Avalonia.Platform.ITopLevelImpl.Surfaces + baseline/Avalonia/lib/net10.0/Avalonia.Controls.dll + current/Avalonia/lib/net10.0/Avalonia.Controls.dll + + + CP0006 + P:Avalonia.Platform.IWindowImpl.RequestedDrawnDecorations + baseline/Avalonia/lib/net10.0/Avalonia.Controls.dll + current/Avalonia/lib/net10.0/Avalonia.Controls.dll + + + CP0006 + M:Avalonia.OpenGL.IGlPlatformSurfaceRenderTargetFactory.CanRenderToSurface(Avalonia.OpenGL.IGlContext,Avalonia.Platform.Surfaces.IPlatformRenderSurface) + baseline/Avalonia/lib/net10.0/Avalonia.OpenGL.dll + current/Avalonia/lib/net10.0/Avalonia.OpenGL.dll + + + CP0006 + M:Avalonia.OpenGL.IGlPlatformSurfaceRenderTargetFactory.CreateRenderTarget(Avalonia.OpenGL.IGlContext,Avalonia.Platform.Surfaces.IPlatformRenderSurface) + baseline/Avalonia/lib/net10.0/Avalonia.OpenGL.dll + current/Avalonia/lib/net10.0/Avalonia.OpenGL.dll + + + CP0006 + M:Avalonia.OpenGL.Surfaces.IGlPlatformSurfaceRenderTarget.BeginDraw(Avalonia.Platform.IRenderTarget.RenderTargetSceneInfo) + baseline/Avalonia/lib/net10.0/Avalonia.OpenGL.dll + current/Avalonia/lib/net10.0/Avalonia.OpenGL.dll + CP0006 M:Avalonia.OpenGL.Surfaces.IGlPlatformSurfaceRenderTarget.BeginDraw(System.Nullable{Avalonia.PixelSize}) @@ -2539,6 +4591,24 @@ baseline/Avalonia/lib/net10.0/Avalonia.OpenGL.dll current/Avalonia/lib/net10.0/Avalonia.OpenGL.dll + + CP0006 + M:Avalonia.Vulkan.IVulkanKhrSurfacePlatformSurfaceFactory.CanRenderToSurface(Avalonia.Vulkan.IVulkanPlatformGraphicsContext,Avalonia.Platform.Surfaces.IPlatformRenderSurface) + baseline/Avalonia/lib/net10.0/Avalonia.Vulkan.dll + current/Avalonia/lib/net10.0/Avalonia.Vulkan.dll + + + CP0006 + M:Avalonia.Vulkan.IVulkanKhrSurfacePlatformSurfaceFactory.CreateSurface(Avalonia.Vulkan.IVulkanPlatformGraphicsContext,Avalonia.Platform.Surfaces.IPlatformRenderSurface) + baseline/Avalonia/lib/net10.0/Avalonia.Vulkan.dll + current/Avalonia/lib/net10.0/Avalonia.Vulkan.dll + + + CP0006 + M:Avalonia.Vulkan.IVulkanPlatformGraphicsContext.CreateRenderTarget(System.Collections.Generic.IEnumerable{Avalonia.Platform.Surfaces.IPlatformRenderSurface}) + baseline/Avalonia/lib/net10.0/Avalonia.Vulkan.dll + current/Avalonia/lib/net10.0/Avalonia.Vulkan.dll + CP0006 M:Avalonia.Input.Platform.IClipboard.SetDataAsync(Avalonia.Input.IAsyncDataTransfer) @@ -2583,21 +4653,63 @@ CP0006 - M:Avalonia.OpenGL.IGlExternalSemaphore.SignalTimelineSemaphore(Avalonia.OpenGL.IGlExternalImageTexture,System.UInt64) - baseline/Avalonia/lib/net6.0/Avalonia.OpenGL.dll - current/Avalonia/lib/net6.0/Avalonia.OpenGL.dll + M:Avalonia.OpenGL.IGlExternalSemaphore.SignalTimelineSemaphore(Avalonia.OpenGL.IGlExternalImageTexture,System.UInt64) + baseline/Avalonia/lib/net6.0/Avalonia.OpenGL.dll + current/Avalonia/lib/net6.0/Avalonia.OpenGL.dll + + + CP0006 + M:Avalonia.OpenGL.IGlExternalSemaphore.WaitTimelineSemaphore(Avalonia.OpenGL.IGlExternalImageTexture,System.UInt64) + baseline/Avalonia/lib/net6.0/Avalonia.OpenGL.dll + current/Avalonia/lib/net6.0/Avalonia.OpenGL.dll + + + CP0006 + P:Avalonia.OpenGL.IGlExternalImageTexture.TextureType + baseline/Avalonia/lib/net6.0/Avalonia.OpenGL.dll + current/Avalonia/lib/net6.0/Avalonia.OpenGL.dll + + + CP0006 + E:Avalonia.Input.IInputElement.GotFocus + baseline/Avalonia/lib/net8.0/Avalonia.Base.dll + current/Avalonia/lib/net8.0/Avalonia.Base.dll + + + CP0006 + E:Avalonia.Input.IInputElement.LostFocus + baseline/Avalonia/lib/net8.0/Avalonia.Base.dll + current/Avalonia/lib/net8.0/Avalonia.Base.dll + + + CP0006 + M:Avalonia.Input.IFocusManager.FindFirstFocusableElement + baseline/Avalonia/lib/net8.0/Avalonia.Base.dll + current/Avalonia/lib/net8.0/Avalonia.Base.dll + + + CP0006 + M:Avalonia.Input.IFocusManager.FindLastFocusableElement + baseline/Avalonia/lib/net8.0/Avalonia.Base.dll + current/Avalonia/lib/net8.0/Avalonia.Base.dll + + + CP0006 + M:Avalonia.Input.IFocusManager.FindNextElement(Avalonia.Input.NavigationDirection,Avalonia.Input.FindNextElementOptions) + baseline/Avalonia/lib/net8.0/Avalonia.Base.dll + current/Avalonia/lib/net8.0/Avalonia.Base.dll CP0006 - M:Avalonia.OpenGL.IGlExternalSemaphore.WaitTimelineSemaphore(Avalonia.OpenGL.IGlExternalImageTexture,System.UInt64) - baseline/Avalonia/lib/net6.0/Avalonia.OpenGL.dll - current/Avalonia/lib/net6.0/Avalonia.OpenGL.dll + M:Avalonia.Input.IFocusManager.Focus(Avalonia.Input.IInputElement,Avalonia.Input.NavigationMethod,Avalonia.Input.KeyModifiers) + baseline/Avalonia/lib/net8.0/Avalonia.Base.dll + current/Avalonia/lib/net8.0/Avalonia.Base.dll CP0006 - P:Avalonia.OpenGL.IGlExternalImageTexture.TextureType - baseline/Avalonia/lib/net6.0/Avalonia.OpenGL.dll - current/Avalonia/lib/net6.0/Avalonia.OpenGL.dll + M:Avalonia.Input.IFocusManager.TryMoveFocus(Avalonia.Input.NavigationDirection,Avalonia.Input.FindNextElementOptions) + baseline/Avalonia/lib/net8.0/Avalonia.Base.dll + current/Avalonia/lib/net8.0/Avalonia.Base.dll CP0006 @@ -2629,6 +4741,12 @@ baseline/Avalonia/lib/net8.0/Avalonia.Base.dll current/Avalonia/lib/net8.0/Avalonia.Base.dll + + CP0006 + M:Avalonia.Platform.ICursorFactory.CreateCursor(Avalonia.Media.Imaging.Bitmap,Avalonia.PixelPoint) + baseline/Avalonia/lib/net8.0/Avalonia.Base.dll + current/Avalonia/lib/net8.0/Avalonia.Base.dll + CP0006 M:Avalonia.Platform.IDrawingContextImpl.PopTextOptions @@ -2647,6 +4765,12 @@ baseline/Avalonia/lib/net8.0/Avalonia.Base.dll current/Avalonia/lib/net8.0/Avalonia.Base.dll + + CP0006 + M:Avalonia.Platform.IDrawingContextLayerImpl.CreateDrawingContext + baseline/Avalonia/lib/net8.0/Avalonia.Base.dll + current/Avalonia/lib/net8.0/Avalonia.Base.dll + CP0006 M:Avalonia.Platform.IFontManagerImpl.TryMatchCharacter(System.Int32,Avalonia.Media.FontStyle,Avalonia.Media.FontWeight,Avalonia.Media.FontStretch,System.String,System.Globalization.CultureInfo,Avalonia.Media.Typeface@) @@ -2683,6 +4807,12 @@ baseline/Avalonia/lib/net8.0/Avalonia.Base.dll current/Avalonia/lib/net8.0/Avalonia.Base.dll + + CP0006 + M:Avalonia.Platform.IPlatformRenderInterfaceContext.CreateRenderTarget(System.Collections.Generic.IEnumerable{Avalonia.Platform.Surfaces.IPlatformRenderSurface}) + baseline/Avalonia/lib/net8.0/Avalonia.Base.dll + current/Avalonia/lib/net8.0/Avalonia.Base.dll + CP0006 M:Avalonia.Platform.IPlatformRenderInterfaceImportedImage.SnapshotWithTimelineSemaphores(Avalonia.Platform.IPlatformRenderInterfaceImportedSemaphore,System.UInt64,Avalonia.Platform.IPlatformRenderInterfaceImportedSemaphore,System.UInt64) @@ -2695,12 +4825,48 @@ baseline/Avalonia/lib/net8.0/Avalonia.Base.dll current/Avalonia/lib/net8.0/Avalonia.Base.dll + + CP0006 + M:Avalonia.Platform.IRenderTarget.CreateDrawingContext(Avalonia.Platform.IRenderTarget.RenderTargetSceneInfo,Avalonia.Platform.RenderTargetDrawingContextProperties@) + baseline/Avalonia/lib/net8.0/Avalonia.Base.dll + current/Avalonia/lib/net8.0/Avalonia.Base.dll + + + CP0006 + M:Avalonia.Platform.IRenderTargetBitmapImpl.CreateDrawingContext + baseline/Avalonia/lib/net8.0/Avalonia.Base.dll + current/Avalonia/lib/net8.0/Avalonia.Base.dll + CP0006 M:Avalonia.Platform.Storage.IStorageProvider.SaveFilePickerWithResultAsync(Avalonia.Platform.Storage.FilePickerSaveOptions) baseline/Avalonia/lib/net8.0/Avalonia.Base.dll current/Avalonia/lib/net8.0/Avalonia.Base.dll + + CP0006 + M:Avalonia.Rendering.IRenderTimer.Start + baseline/Avalonia/lib/net8.0/Avalonia.Base.dll + current/Avalonia/lib/net8.0/Avalonia.Base.dll + + + CP0006 + M:Avalonia.Rendering.IRenderTimer.Stop + baseline/Avalonia/lib/net8.0/Avalonia.Base.dll + current/Avalonia/lib/net8.0/Avalonia.Base.dll + + + CP0006 + P:Avalonia.Input.IInputRoot.FocusRoot + baseline/Avalonia/lib/net8.0/Avalonia.Base.dll + current/Avalonia/lib/net8.0/Avalonia.Base.dll + + + CP0006 + P:Avalonia.Platform.IDrawingContextLayerImpl.IsCorrupted + baseline/Avalonia/lib/net8.0/Avalonia.Base.dll + current/Avalonia/lib/net8.0/Avalonia.Base.dll + CP0006 P:Avalonia.Platform.ILockedFramebuffer.AlphaFormat @@ -2725,12 +4891,30 @@ baseline/Avalonia/lib/net8.0/Avalonia.Base.dll current/Avalonia/lib/net8.0/Avalonia.Base.dll + + CP0006 + M:Avalonia.Platform.IWindowImpl.SetWindowDecorations(Avalonia.Controls.WindowDecorations) + baseline/Avalonia/lib/net8.0/Avalonia.Controls.dll + current/Avalonia/lib/net8.0/Avalonia.Controls.dll + CP0006 M:Avalonia.Platform.IWindowingPlatform.GetWindowsZOrder(System.ReadOnlySpan{Avalonia.Platform.IWindowImpl},System.Span{System.Int64}) baseline/Avalonia/lib/net8.0/Avalonia.Controls.dll current/Avalonia/lib/net8.0/Avalonia.Controls.dll + + CP0006 + P:Avalonia.Platform.ITopLevelImpl.Surfaces + baseline/Avalonia/lib/net8.0/Avalonia.Controls.dll + current/Avalonia/lib/net8.0/Avalonia.Controls.dll + + + CP0006 + P:Avalonia.Platform.IWindowImpl.RequestedDrawnDecorations + baseline/Avalonia/lib/net8.0/Avalonia.Controls.dll + current/Avalonia/lib/net8.0/Avalonia.Controls.dll + CP0006 M:Avalonia.OpenGL.IGlExternalSemaphore.SignalTimelineSemaphore(Avalonia.OpenGL.IGlExternalImageTexture,System.UInt64) @@ -2743,6 +4927,24 @@ baseline/Avalonia/lib/net8.0/Avalonia.OpenGL.dll current/Avalonia/lib/net8.0/Avalonia.OpenGL.dll + + CP0006 + M:Avalonia.OpenGL.IGlPlatformSurfaceRenderTargetFactory.CanRenderToSurface(Avalonia.OpenGL.IGlContext,Avalonia.Platform.Surfaces.IPlatformRenderSurface) + baseline/Avalonia/lib/net8.0/Avalonia.OpenGL.dll + current/Avalonia/lib/net8.0/Avalonia.OpenGL.dll + + + CP0006 + M:Avalonia.OpenGL.IGlPlatformSurfaceRenderTargetFactory.CreateRenderTarget(Avalonia.OpenGL.IGlContext,Avalonia.Platform.Surfaces.IPlatformRenderSurface) + baseline/Avalonia/lib/net8.0/Avalonia.OpenGL.dll + current/Avalonia/lib/net8.0/Avalonia.OpenGL.dll + + + CP0006 + M:Avalonia.OpenGL.Surfaces.IGlPlatformSurfaceRenderTarget.BeginDraw(Avalonia.Platform.IRenderTarget.RenderTargetSceneInfo) + baseline/Avalonia/lib/net8.0/Avalonia.OpenGL.dll + current/Avalonia/lib/net8.0/Avalonia.OpenGL.dll + CP0006 M:Avalonia.OpenGL.Surfaces.IGlPlatformSurfaceRenderTarget.BeginDraw(System.Nullable{Avalonia.PixelSize}) @@ -2761,6 +4963,24 @@ baseline/Avalonia/lib/net8.0/Avalonia.OpenGL.dll current/Avalonia/lib/net8.0/Avalonia.OpenGL.dll + + CP0006 + M:Avalonia.Vulkan.IVulkanKhrSurfacePlatformSurfaceFactory.CanRenderToSurface(Avalonia.Vulkan.IVulkanPlatformGraphicsContext,Avalonia.Platform.Surfaces.IPlatformRenderSurface) + baseline/Avalonia/lib/net8.0/Avalonia.Vulkan.dll + current/Avalonia/lib/net8.0/Avalonia.Vulkan.dll + + + CP0006 + M:Avalonia.Vulkan.IVulkanKhrSurfacePlatformSurfaceFactory.CreateSurface(Avalonia.Vulkan.IVulkanPlatformGraphicsContext,Avalonia.Platform.Surfaces.IPlatformRenderSurface) + baseline/Avalonia/lib/net8.0/Avalonia.Vulkan.dll + current/Avalonia/lib/net8.0/Avalonia.Vulkan.dll + + + CP0006 + M:Avalonia.Vulkan.IVulkanPlatformGraphicsContext.CreateRenderTarget(System.Collections.Generic.IEnumerable{Avalonia.Platform.Surfaces.IPlatformRenderSurface}) + baseline/Avalonia/lib/net8.0/Avalonia.Vulkan.dll + current/Avalonia/lib/net8.0/Avalonia.Vulkan.dll + CP0006 M:Avalonia.Input.Platform.IClipboard.SetDataAsync(Avalonia.Input.IAsyncDataTransfer) @@ -2839,6 +5059,12 @@ baseline/Avalonia/lib/net10.0/Avalonia.Base.dll current/Avalonia/lib/net10.0/Avalonia.Base.dll + + CP0008 + T:Avalonia.Input.IInputRoot + baseline/Avalonia/lib/net10.0/Avalonia.Base.dll + current/Avalonia/lib/net10.0/Avalonia.Base.dll + CP0008 T:Avalonia.Media.ImmediateDrawingContext @@ -2851,6 +5077,18 @@ baseline/Avalonia/lib/net10.0/Avalonia.Base.dll current/Avalonia/lib/net10.0/Avalonia.Base.dll + + CP0008 + T:Avalonia.Platform.IDrawingContextLayerImpl + baseline/Avalonia/lib/net10.0/Avalonia.Base.dll + current/Avalonia/lib/net10.0/Avalonia.Base.dll + + + CP0008 + T:Avalonia.Platform.IDrawingContextLayerWithRenderContextAffinityImpl + baseline/Avalonia/lib/net10.0/Avalonia.Base.dll + current/Avalonia/lib/net10.0/Avalonia.Base.dll + CP0008 T:Avalonia.Platform.IPlatformGraphicsContext @@ -2869,6 +5107,12 @@ baseline/Avalonia/lib/net10.0/Avalonia.Base.dll current/Avalonia/lib/net10.0/Avalonia.Base.dll + + CP0008 + T:Avalonia.Platform.IRenderTargetBitmapImpl + baseline/Avalonia/lib/net10.0/Avalonia.Base.dll + current/Avalonia/lib/net10.0/Avalonia.Base.dll + CP0008 T:Avalonia.Platform.IWriteableBitmapImpl @@ -2881,6 +5125,12 @@ baseline/Avalonia/lib/net10.0/Avalonia.Controls.dll current/Avalonia/lib/net10.0/Avalonia.Controls.dll + + CP0008 + T:Avalonia.Controls.Embedding.EmbeddableControlRoot + baseline/Avalonia/lib/net10.0/Avalonia.Controls.dll + current/Avalonia/lib/net10.0/Avalonia.Controls.dll + CP0008 T:Avalonia.Controls.Embedding.Offscreen.OffscreenTopLevelImplBase @@ -2893,6 +5143,36 @@ baseline/Avalonia/lib/net10.0/Avalonia.Controls.dll current/Avalonia/lib/net10.0/Avalonia.Controls.dll + + CP0008 + T:Avalonia.Controls.Primitives.OverlayPopupHost + baseline/Avalonia/lib/net10.0/Avalonia.Controls.dll + current/Avalonia/lib/net10.0/Avalonia.Controls.dll + + + CP0008 + T:Avalonia.Controls.Primitives.PopupRoot + baseline/Avalonia/lib/net10.0/Avalonia.Controls.dll + current/Avalonia/lib/net10.0/Avalonia.Controls.dll + + + CP0008 + T:Avalonia.Controls.TopLevel + baseline/Avalonia/lib/net10.0/Avalonia.Controls.dll + current/Avalonia/lib/net10.0/Avalonia.Controls.dll + + + CP0008 + T:Avalonia.Controls.Window + baseline/Avalonia/lib/net10.0/Avalonia.Controls.dll + current/Avalonia/lib/net10.0/Avalonia.Controls.dll + + + CP0008 + T:Avalonia.Controls.WindowBase + baseline/Avalonia/lib/net10.0/Avalonia.Controls.dll + current/Avalonia/lib/net10.0/Avalonia.Controls.dll + CP0008 T:Avalonia.Platform.IPopupImpl @@ -2917,6 +5197,12 @@ baseline/Avalonia/lib/net10.0/Avalonia.Controls.dll current/Avalonia/lib/net10.0/Avalonia.Controls.dll + + CP0008 + T:Avalonia.Dialogs.AboutAvaloniaDialog + baseline/Avalonia/lib/net10.0/Avalonia.Dialogs.dll + current/Avalonia/lib/net10.0/Avalonia.Dialogs.dll + CP0008 T:Avalonia.Metal.IMetalDevice @@ -2959,6 +5245,12 @@ baseline/Avalonia/lib/net8.0/Avalonia.Base.dll current/Avalonia/lib/net8.0/Avalonia.Base.dll + + CP0008 + T:Avalonia.Input.IInputRoot + baseline/Avalonia/lib/net8.0/Avalonia.Base.dll + current/Avalonia/lib/net8.0/Avalonia.Base.dll + CP0008 T:Avalonia.Media.ImmediateDrawingContext @@ -2971,6 +5263,18 @@ baseline/Avalonia/lib/net8.0/Avalonia.Base.dll current/Avalonia/lib/net8.0/Avalonia.Base.dll + + CP0008 + T:Avalonia.Platform.IDrawingContextLayerImpl + baseline/Avalonia/lib/net8.0/Avalonia.Base.dll + current/Avalonia/lib/net8.0/Avalonia.Base.dll + + + CP0008 + T:Avalonia.Platform.IDrawingContextLayerWithRenderContextAffinityImpl + baseline/Avalonia/lib/net8.0/Avalonia.Base.dll + current/Avalonia/lib/net8.0/Avalonia.Base.dll + CP0008 T:Avalonia.Platform.IPlatformGraphicsContext @@ -2989,6 +5293,12 @@ baseline/Avalonia/lib/net8.0/Avalonia.Base.dll current/Avalonia/lib/net8.0/Avalonia.Base.dll + + CP0008 + T:Avalonia.Platform.IRenderTargetBitmapImpl + baseline/Avalonia/lib/net8.0/Avalonia.Base.dll + current/Avalonia/lib/net8.0/Avalonia.Base.dll + CP0008 T:Avalonia.Platform.IWriteableBitmapImpl @@ -3001,6 +5311,12 @@ baseline/Avalonia/lib/net8.0/Avalonia.Controls.dll current/Avalonia/lib/net8.0/Avalonia.Controls.dll + + CP0008 + T:Avalonia.Controls.Embedding.EmbeddableControlRoot + baseline/Avalonia/lib/net8.0/Avalonia.Controls.dll + current/Avalonia/lib/net8.0/Avalonia.Controls.dll + CP0008 T:Avalonia.Controls.Embedding.Offscreen.OffscreenTopLevelImplBase @@ -3013,6 +5329,36 @@ baseline/Avalonia/lib/net8.0/Avalonia.Controls.dll current/Avalonia/lib/net8.0/Avalonia.Controls.dll + + CP0008 + T:Avalonia.Controls.Primitives.OverlayPopupHost + baseline/Avalonia/lib/net8.0/Avalonia.Controls.dll + current/Avalonia/lib/net8.0/Avalonia.Controls.dll + + + CP0008 + T:Avalonia.Controls.Primitives.PopupRoot + baseline/Avalonia/lib/net8.0/Avalonia.Controls.dll + current/Avalonia/lib/net8.0/Avalonia.Controls.dll + + + CP0008 + T:Avalonia.Controls.TopLevel + baseline/Avalonia/lib/net8.0/Avalonia.Controls.dll + current/Avalonia/lib/net8.0/Avalonia.Controls.dll + + + CP0008 + T:Avalonia.Controls.Window + baseline/Avalonia/lib/net8.0/Avalonia.Controls.dll + current/Avalonia/lib/net8.0/Avalonia.Controls.dll + + + CP0008 + T:Avalonia.Controls.WindowBase + baseline/Avalonia/lib/net8.0/Avalonia.Controls.dll + current/Avalonia/lib/net8.0/Avalonia.Controls.dll + CP0008 T:Avalonia.Platform.IPopupImpl @@ -3037,6 +5383,12 @@ baseline/Avalonia/lib/net8.0/Avalonia.Controls.dll current/Avalonia/lib/net8.0/Avalonia.Controls.dll + + CP0008 + T:Avalonia.Dialogs.AboutAvaloniaDialog + baseline/Avalonia/lib/net8.0/Avalonia.Dialogs.dll + current/Avalonia/lib/net8.0/Avalonia.Dialogs.dll + CP0008 T:Avalonia.Metal.IMetalDevice @@ -3085,6 +5437,30 @@ baseline/Avalonia/lib/net10.0/Avalonia.Base.dll current/Avalonia/lib/net10.0/Avalonia.Base.dll + + CP0009 + T:Avalonia.Input.HoldingRoutedEventArgs + baseline/Avalonia/lib/net10.0/Avalonia.Base.dll + current/Avalonia/lib/net10.0/Avalonia.Base.dll + + + CP0009 + T:Avalonia.Controls.Primitives.AdornerLayer + baseline/Avalonia/lib/net10.0/Avalonia.Controls.dll + current/Avalonia/lib/net10.0/Avalonia.Controls.dll + + + CP0009 + T:Avalonia.Controls.Primitives.OverlayPopupHost + baseline/Avalonia/lib/net10.0/Avalonia.Controls.dll + current/Avalonia/lib/net10.0/Avalonia.Controls.dll + + + CP0009 + T:Avalonia.Controls.Primitives.VisualLayerManager + baseline/Avalonia/lib/net10.0/Avalonia.Controls.dll + current/Avalonia/lib/net10.0/Avalonia.Controls.dll + CP0009 T:Avalonia.Platform.Screen @@ -3103,6 +5479,30 @@ baseline/Avalonia/lib/net8.0/Avalonia.Base.dll current/Avalonia/lib/net8.0/Avalonia.Base.dll + + CP0009 + T:Avalonia.Input.HoldingRoutedEventArgs + baseline/Avalonia/lib/net8.0/Avalonia.Base.dll + current/Avalonia/lib/net8.0/Avalonia.Base.dll + + + CP0009 + T:Avalonia.Controls.Primitives.AdornerLayer + baseline/Avalonia/lib/net8.0/Avalonia.Controls.dll + current/Avalonia/lib/net8.0/Avalonia.Controls.dll + + + CP0009 + T:Avalonia.Controls.Primitives.OverlayPopupHost + baseline/Avalonia/lib/net8.0/Avalonia.Controls.dll + current/Avalonia/lib/net8.0/Avalonia.Controls.dll + + + CP0009 + T:Avalonia.Controls.Primitives.VisualLayerManager + baseline/Avalonia/lib/net8.0/Avalonia.Controls.dll + current/Avalonia/lib/net8.0/Avalonia.Controls.dll + CP0009 T:Avalonia.Platform.Screen diff --git a/build/AnalyzerProject.targets b/build/AnalyzerProject.targets index 4a95cb306e..5bf69d62d2 100644 --- a/build/AnalyzerProject.targets +++ b/build/AnalyzerProject.targets @@ -8,7 +8,7 @@ - + diff --git a/build/Base.props b/build/Base.props index ab5853fcfa..cd0aa8196e 100644 --- a/build/Base.props +++ b/build/Base.props @@ -1,8 +1,8 @@  - - - + + + diff --git a/build/Binding.props b/build/Binding.props deleted file mode 100644 index 9e19e86d89..0000000000 --- a/build/Binding.props +++ /dev/null @@ -1,5 +0,0 @@ - - - - - diff --git a/build/HarfBuzzSharp.props b/build/HarfBuzzSharp.props index c7a3d753f2..7e0b4e3e19 100644 --- a/build/HarfBuzzSharp.props +++ b/build/HarfBuzzSharp.props @@ -1,7 +1,7 @@  - - - + + + diff --git a/build/ImageSharp.props b/build/ImageSharp.props deleted file mode 100644 index cf401630b0..0000000000 --- a/build/ImageSharp.props +++ /dev/null @@ -1,5 +0,0 @@ - - - - - diff --git a/build/LegacyProject.targets b/build/LegacyProject.targets deleted file mode 100644 index 0e0d49b1c2..0000000000 --- a/build/LegacyProject.targets +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/build/Microsoft.CSharp.props b/build/Microsoft.CSharp.props deleted file mode 100644 index 4f738dd254..0000000000 --- a/build/Microsoft.CSharp.props +++ /dev/null @@ -1,5 +0,0 @@ - - - - - diff --git a/build/Microsoft.Reactive.Testing.props b/build/Microsoft.Reactive.Testing.props deleted file mode 100644 index c39c72df77..0000000000 --- a/build/Microsoft.Reactive.Testing.props +++ /dev/null @@ -1,5 +0,0 @@ - - - - - diff --git a/build/Moq.props b/build/Moq.props deleted file mode 100644 index fc659f7f5f..0000000000 --- a/build/Moq.props +++ /dev/null @@ -1,5 +0,0 @@ - - - - - diff --git a/build/NetCore.props b/build/NetCore.props deleted file mode 100644 index b9cde28015..0000000000 --- a/build/NetCore.props +++ /dev/null @@ -1,4 +0,0 @@ - - - - diff --git a/build/NetFX.props b/build/NetFX.props deleted file mode 100644 index 14adb54035..0000000000 --- a/build/NetFX.props +++ /dev/null @@ -1,6 +0,0 @@ - - - - - - diff --git a/build/Rx.props b/build/Rx.props deleted file mode 100644 index 462428c286..0000000000 --- a/build/Rx.props +++ /dev/null @@ -1,5 +0,0 @@ - - - - - diff --git a/build/SampleApp.props b/build/SampleApp.props index 3f44553d2d..6816a38cbe 100644 --- a/build/SampleApp.props +++ b/build/SampleApp.props @@ -14,7 +14,7 @@ - + diff --git a/build/SharedVersion.props b/build/SharedVersion.props index 37d14a5647..b8c0dd4d43 100644 --- a/build/SharedVersion.props +++ b/build/SharedVersion.props @@ -8,7 +8,7 @@ https://avaloniaui.net/?utm_source=nuget&utm_medium=referral&utm_content=project_homepage_link https://github.com/AvaloniaUI/Avalonia/ true - $(NoWarn);CS1591 + $(NoWarn);CS1591;NU5104 MIT Icon.png 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. diff --git a/build/SharpDX.props b/build/SharpDX.props deleted file mode 100644 index ff521977fd..0000000000 --- a/build/SharpDX.props +++ /dev/null @@ -1,14 +0,0 @@ - - - 4.0.1 - - - - - - - - - - - diff --git a/build/SkiaSharp.props b/build/SkiaSharp.props index 0dc94b3239..c2b04d3026 100644 --- a/build/SkiaSharp.props +++ b/build/SkiaSharp.props @@ -1,7 +1,7 @@  - - - + + + diff --git a/build/UnitTests.NetFX.props b/build/UnitTests.NetFX.props index e9a29d80fe..22c40ecbbb 100644 --- a/build/UnitTests.NetFX.props +++ b/build/UnitTests.NetFX.props @@ -1,5 +1,4 @@ - PreserveNewest diff --git a/build/XUnit.props b/build/XUnit.props index 5c63ed69db..42b5cae7dc 100644 --- a/build/XUnit.props +++ b/build/XUnit.props @@ -1,7 +1,7 @@  - + diff --git a/build/readme.md b/build/readme.md index e147556b1c..46399914f4 100644 --- a/build/readme.md +++ b/build/readme.md @@ -4,12 +4,6 @@ - - - - - - diff --git a/dirs.proj b/dirs.proj index 084fd30475..20df84439e 100644 --- a/dirs.proj +++ b/dirs.proj @@ -22,7 +22,4 @@ - - - diff --git a/external/Avalonia.DBus b/external/Avalonia.DBus new file mode 160000 index 0000000000..f91a822c25 --- /dev/null +++ b/external/Avalonia.DBus @@ -0,0 +1 @@ +Subproject commit f91a822c258476f185e51112388775591e6ef9d6 diff --git a/external/Numerge b/external/Numerge index 9738c6121f..5530e1cbe9 160000 --- a/external/Numerge +++ b/external/Numerge @@ -1 +1 @@ -Subproject commit 9738c6121fdd143c78d3e25686a7e4e13c00f586 +Subproject commit 5530e1cbe9e105ff4ebc9da1f4af3253a8756754 diff --git a/external/XamlX b/external/XamlX index c32d3040e5..009d481547 160000 --- a/external/XamlX +++ b/external/XamlX @@ -1 +1 @@ -Subproject commit c32d3040e536ae9768233ea5a445697632578bd0 +Subproject commit 009d4815470cf4bf71d1adbb633a5d81dcb2bb52 diff --git a/global.json b/global.json index 3773c7d736..f6ed3dfdfb 100644 --- a/global.json +++ b/global.json @@ -1,6 +1,6 @@ { "sdk": { - "version": "10.0.101", + "version": "10.0.201", "rollForward": "latestFeature" }, "test": { diff --git a/native/Avalonia.Native/src/OSX/StorageProvider.mm b/native/Avalonia.Native/src/OSX/StorageProvider.mm index 570281fdfd..e0a471a4f3 100644 --- a/native/Avalonia.Native/src/OSX/StorageProvider.mm +++ b/native/Avalonia.Native/src/OSX/StorageProvider.mm @@ -148,8 +148,23 @@ public: [fileUri stopAccessingSecurityScopedResource]; } } + + static NSWindow* GetEffectiveNSWindow(IAvnTopLevel* topLevel) + { + auto windowHolder = dynamic_cast(topLevel); + if (windowHolder != nullptr) + return windowHolder->GetNSWindow(); + + auto viewHolder = dynamic_cast(topLevel); + if (viewHolder != nullptr) { + auto view = (NSView*)viewHolder->GetNSView(); + return [view window]; + } + + return nullptr; + } - virtual void SelectFolderDialog (IAvnWindow* parentWindowHandle, + virtual void SelectFolderDialog (IAvnTopLevel* parentTopLevel, IAvnSystemDialogEvents* events, bool allowMultiple, const char* title, @@ -176,6 +191,8 @@ public: panel.directoryURL = [NSURL URLWithString:directoryString]; } + auto parentWindow = GetEffectiveNSWindow(parentTopLevel); + auto handler = ^(NSModalResponse result) { if(result == NSFileHandlingPanelOKButton) { @@ -188,10 +205,9 @@ public: [panel orderOut:panel]; - if(parentWindowHandle != nullptr) + if (parentWindow != nullptr) { - auto windowHolder = dynamic_cast(parentWindowHandle); - [windowHolder->GetNSWindow() makeKeyAndOrderFront:windowHolder->GetNSWindow()]; + [parentWindow makeKeyAndOrderFront:parentWindow]; } return; @@ -202,11 +218,9 @@ public: }; - if(parentWindowHandle != nullptr) + if (parentWindow != nullptr) { - auto windowBase = dynamic_cast(parentWindowHandle); - - [panel beginSheetModalForWindow:windowBase->GetNSWindow() completionHandler:handler]; + [panel beginSheetModalForWindow:parentWindow completionHandler:handler]; } else { @@ -215,7 +229,7 @@ public: } } - virtual void OpenFileDialog (IAvnWindow* parentWindowHandle, + virtual void OpenFileDialog (IAvnTopLevel* parentTopLevel, IAvnSystemDialogEvents* events, bool allowMultiple, const char* title, @@ -249,6 +263,8 @@ public: panel.directoryURL = [NSURL URLWithString:directoryString]; } + auto parentWindow = GetEffectiveNSWindow(parentTopLevel); + auto handler = ^(NSModalResponse result) { if(result == NSFileHandlingPanelOKButton) { @@ -261,10 +277,9 @@ public: [panel orderOut:panel]; - if(parentWindowHandle != nullptr) + if (parentWindow != nullptr) { - auto windowHolder = dynamic_cast(parentWindowHandle); - [windowHolder->GetNSWindow() makeKeyAndOrderFront:windowHolder->GetNSWindow()]; + [parentWindow makeKeyAndOrderFront:parentWindow]; } return; @@ -275,11 +290,9 @@ public: }; - if(parentWindowHandle != nullptr) + if (parentWindow != nullptr) { - auto windowHolder = dynamic_cast(parentWindowHandle); - - [panel beginSheetModalForWindow:windowHolder->GetNSWindow() completionHandler:handler]; + [panel beginSheetModalForWindow:parentWindow completionHandler:handler]; } else { @@ -288,7 +301,7 @@ public: } } - virtual void SaveFileDialog (IAvnWindow* parentWindowHandle, + virtual void SaveFileDialog (IAvnTopLevel* parentTopLevel, IAvnSystemDialogEvents* events, const char* title, const char* initialDirectory, @@ -319,6 +332,8 @@ public: panel.directoryURL = [NSURL URLWithString:directoryString]; } + auto parentWindow = GetEffectiveNSWindow(parentTopLevel); + auto handler = ^(NSModalResponse result) { int selectedIndex = -1; if (panel.accessoryView != nil) @@ -339,10 +354,9 @@ public: [panel orderOut:panel]; - if(parentWindowHandle != nullptr) + if (parentWindow != nullptr) { - auto windowHolder = dynamic_cast(parentWindowHandle); - [windowHolder->GetNSWindow() makeKeyAndOrderFront:windowHolder->GetNSWindow()]; + [parentWindow makeKeyAndOrderFront:parentWindow]; } return; @@ -352,11 +366,9 @@ public: }; - if(parentWindowHandle != nullptr) + if (parentWindow != nullptr) { - auto windowBase = dynamic_cast(parentWindowHandle); - - [panel beginSheetModalForWindow:windowBase->GetNSWindow() completionHandler:handler]; + [panel beginSheetModalForWindow:parentWindow completionHandler:handler]; } else { diff --git a/native/Avalonia.Native/src/OSX/WindowImpl.h b/native/Avalonia.Native/src/OSX/WindowImpl.h index 940699f09d..353ee4d2f1 100644 --- a/native/Avalonia.Native/src/OSX/WindowImpl.h +++ b/native/Avalonia.Native/src/OSX/WindowImpl.h @@ -62,8 +62,6 @@ BEGIN_INTERFACE_MAP() virtual HRESULT SetExtendClientArea (bool enable) override; - virtual HRESULT SetExtendClientAreaHints (AvnExtendClientAreaChromeHints hints) override; - virtual HRESULT GetExtendTitleBarHeight (double*ret) override; virtual HRESULT SetExtendTitleBarHeight (double value) override; @@ -110,7 +108,6 @@ private: NSRect _preZoomSize; bool _transitioningWindowState; bool _isClientAreaExtended; - AvnExtendClientAreaChromeHints _extendClientHints; bool _isModal; }; diff --git a/native/Avalonia.Native/src/OSX/WindowImpl.mm b/native/Avalonia.Native/src/OSX/WindowImpl.mm index 5a57715b55..f42b7c27cb 100644 --- a/native/Avalonia.Native/src/OSX/WindowImpl.mm +++ b/native/Avalonia.Native/src/OSX/WindowImpl.mm @@ -13,7 +13,6 @@ WindowImpl::WindowImpl(IAvnWindowEvents *events) : TopLevelImpl(events), WindowBaseImpl(events, false) { _isEnabled = true; _isClientAreaExtended = false; - _extendClientHints = AvnDefaultChrome; _fullScreenActive = false; _canResize = true; _canMinimize = true; @@ -153,20 +152,11 @@ void WindowImpl::WindowStateChanged() { if (_isClientAreaExtended) { if (_lastWindowState == FullScreen) { // we exited fs. - if (_extendClientHints & AvnOSXThickTitleBar) { - Window.toolbar = [NSToolbar new]; - Window.toolbar.showsBaselineSeparator = false; - } - [Window setTitlebarAppearsTransparent:true]; [StandardContainer setFrameSize:StandardContainer.frame.size]; } else if (state == FullScreen) { // we entered fs. - if (_extendClientHints & AvnOSXThickTitleBar) { - Window.toolbar = nullptr; - } - [Window setTitlebarAppearsTransparent:false]; [StandardContainer setFrameSize:StandardContainer.frame.size]; @@ -240,6 +230,10 @@ HRESULT WindowImpl::SetDecorations(SystemDecorations value) { UpdateAppearance(); + if (_isClientAreaExtended) { + [StandardContainer ShowTitleBar:_decorations == SystemDecorationsFull]; + } + switch (_decorations) { case SystemDecorationsNone: [Window setHasShadow:NO]; @@ -391,20 +385,9 @@ HRESULT WindowImpl::SetExtendClientArea(bool enable) { [Window setTitlebarAppearsTransparent:true]; - auto wantsTitleBar = (_extendClientHints & AvnSystemChrome) || (_extendClientHints & AvnPreferSystemChrome); - - if (wantsTitleBar) { - [StandardContainer ShowTitleBar:true]; - } else { - [StandardContainer ShowTitleBar:false]; - } + [StandardContainer ShowTitleBar:_decorations == SystemDecorationsFull]; - if (_extendClientHints & AvnOSXThickTitleBar) { - Window.toolbar = [NSToolbar new]; - Window.toolbar.showsBaselineSeparator = false; - } else { - Window.toolbar = nullptr; - } + Window.toolbar = nullptr; } else { Window.titleVisibility = NSWindowTitleVisible; Window.toolbar = nullptr; @@ -420,17 +403,6 @@ HRESULT WindowImpl::SetExtendClientArea(bool enable) { } } -HRESULT WindowImpl::SetExtendClientAreaHints(AvnExtendClientAreaChromeHints hints) { - START_COM_CALL; - - @autoreleasepool { - _extendClientHints = hints; - - SetExtendClientArea(_isClientAreaExtended); - return S_OK; - } -} - HRESULT WindowImpl::GetExtendTitleBarHeight(double *ret) { START_COM_CALL; @@ -591,6 +563,10 @@ NSWindowStyleMask WindowImpl::CalculateStyleMask() { case SystemDecorationsBorderOnly: s = s | NSWindowStyleMaskTitled | NSWindowStyleMaskFullSizeContentView; + + if (_canResize && _isEnabled) { + s = s | NSWindowStyleMaskResizable; + } break; case SystemDecorationsFull: @@ -619,9 +595,7 @@ void WindowImpl::UpdateAppearance() { return; } - bool wantsChrome = (_extendClientHints & AvnSystemChrome) || (_extendClientHints & AvnPreferSystemChrome); - bool hasTrafficLights = (_decorations == SystemDecorationsFull) && - (_isClientAreaExtended ? wantsChrome : true); + bool hasTrafficLights = (_decorations == SystemDecorationsFull); NSButton* closeButton = [Window standardWindowButton:NSWindowCloseButton]; NSButton* miniaturizeButton = [Window standardWindowButton:NSWindowMiniaturizeButton]; diff --git a/native/Avalonia.Native/src/OSX/metal.mm b/native/Avalonia.Native/src/OSX/metal.mm index 33aa2aeb53..517872b147 100644 --- a/native/Avalonia.Native/src/OSX/metal.mm +++ b/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(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 - - + + + - - - + + + @@ -24,11 +25,6 @@ - - - diff --git a/packages/Avalonia/Avalonia.csproj b/packages/Avalonia/Avalonia.csproj index e59e72f13b..f86a03133a 100644 --- a/packages/Avalonia/Avalonia.csproj +++ b/packages/Avalonia/Avalonia.csproj @@ -5,7 +5,7 @@ - + @@ -71,7 +71,6 @@ - diff --git a/samples/BindingDemo/BindingDemo.csproj b/samples/BindingDemo/BindingDemo.csproj index b12e350814..c38710a5f8 100644 --- a/samples/BindingDemo/BindingDemo.csproj +++ b/samples/BindingDemo/BindingDemo.csproj @@ -10,10 +10,11 @@ + + + - - diff --git a/samples/ControlCatalog.Android/ControlCatalog.Android.csproj b/samples/ControlCatalog.Android/ControlCatalog.Android.csproj index c2fe16970a..a630799c05 100644 --- a/samples/ControlCatalog.Android/ControlCatalog.Android.csproj +++ b/samples/ControlCatalog.Android/ControlCatalog.Android.csproj @@ -25,7 +25,7 @@ - + diff --git a/samples/ControlCatalog.Desktop/ControlCatalog.Desktop.csproj b/samples/ControlCatalog.Desktop/ControlCatalog.Desktop.csproj index c951862ad2..73b983f788 100644 --- a/samples/ControlCatalog.Desktop/ControlCatalog.Desktop.csproj +++ b/samples/ControlCatalog.Desktop/ControlCatalog.Desktop.csproj @@ -26,7 +26,7 @@ - + diff --git a/samples/ControlCatalog/Assets/CurvedHeader/avatar.jpg b/samples/ControlCatalog/Assets/CurvedHeader/avatar.jpg new file mode 100644 index 0000000000..b7b9610f95 Binary files /dev/null and b/samples/ControlCatalog/Assets/CurvedHeader/avatar.jpg differ diff --git a/samples/ControlCatalog/Assets/CurvedHeader/featured.jpg b/samples/ControlCatalog/Assets/CurvedHeader/featured.jpg new file mode 100644 index 0000000000..2bb09f1185 Binary files /dev/null and b/samples/ControlCatalog/Assets/CurvedHeader/featured.jpg differ diff --git a/samples/ControlCatalog/Assets/CurvedHeader/product1.jpg b/samples/ControlCatalog/Assets/CurvedHeader/product1.jpg new file mode 100644 index 0000000000..4447a8a6f2 Binary files /dev/null and b/samples/ControlCatalog/Assets/CurvedHeader/product1.jpg differ diff --git a/samples/ControlCatalog/Assets/CurvedHeader/product2.jpg b/samples/ControlCatalog/Assets/CurvedHeader/product2.jpg new file mode 100644 index 0000000000..58acb3ebf0 Binary files /dev/null and b/samples/ControlCatalog/Assets/CurvedHeader/product2.jpg differ diff --git a/samples/ControlCatalog/Assets/CurvedHeader/product3.jpg b/samples/ControlCatalog/Assets/CurvedHeader/product3.jpg new file mode 100644 index 0000000000..4722989113 Binary files /dev/null and b/samples/ControlCatalog/Assets/CurvedHeader/product3.jpg differ diff --git a/samples/ControlCatalog/Assets/CurvedHeader/update1.jpg b/samples/ControlCatalog/Assets/CurvedHeader/update1.jpg new file mode 100644 index 0000000000..d434d6194b Binary files /dev/null and b/samples/ControlCatalog/Assets/CurvedHeader/update1.jpg differ diff --git a/samples/ControlCatalog/Assets/CurvedHeader/update2.jpg b/samples/ControlCatalog/Assets/CurvedHeader/update2.jpg new file mode 100644 index 0000000000..db35f09e02 Binary files /dev/null and b/samples/ControlCatalog/Assets/CurvedHeader/update2.jpg differ diff --git a/samples/ControlCatalog/Assets/CurvedHeader/update3.jpg b/samples/ControlCatalog/Assets/CurvedHeader/update3.jpg new file mode 100644 index 0000000000..0859309904 Binary files /dev/null and b/samples/ControlCatalog/Assets/CurvedHeader/update3.jpg differ diff --git a/samples/ControlCatalog/Assets/ModernApp/avatar.jpg b/samples/ControlCatalog/Assets/ModernApp/avatar.jpg new file mode 100644 index 0000000000..3c55a5af75 Binary files /dev/null and b/samples/ControlCatalog/Assets/ModernApp/avatar.jpg differ diff --git a/samples/ControlCatalog/Assets/ModernApp/dest_alps.jpg b/samples/ControlCatalog/Assets/ModernApp/dest_alps.jpg new file mode 100644 index 0000000000..ebbac8920b Binary files /dev/null and b/samples/ControlCatalog/Assets/ModernApp/dest_alps.jpg differ diff --git a/samples/ControlCatalog/Assets/ModernApp/dest_forest.jpg b/samples/ControlCatalog/Assets/ModernApp/dest_forest.jpg new file mode 100644 index 0000000000..876be96909 Binary files /dev/null and b/samples/ControlCatalog/Assets/ModernApp/dest_forest.jpg differ diff --git a/samples/ControlCatalog/Assets/ModernApp/dest_norway.jpg b/samples/ControlCatalog/Assets/ModernApp/dest_norway.jpg new file mode 100644 index 0000000000..aec9e2ef36 Binary files /dev/null and b/samples/ControlCatalog/Assets/ModernApp/dest_norway.jpg differ diff --git a/samples/ControlCatalog/Assets/ModernApp/exp_angkor.jpg b/samples/ControlCatalog/Assets/ModernApp/exp_angkor.jpg new file mode 100644 index 0000000000..812859a392 Binary files /dev/null and b/samples/ControlCatalog/Assets/ModernApp/exp_angkor.jpg differ diff --git a/samples/ControlCatalog/Assets/ModernApp/exp_tokyo.jpg b/samples/ControlCatalog/Assets/ModernApp/exp_tokyo.jpg new file mode 100644 index 0000000000..77f73a4af1 Binary files /dev/null and b/samples/ControlCatalog/Assets/ModernApp/exp_tokyo.jpg differ diff --git a/samples/ControlCatalog/Assets/ModernApp/gallery_alpine.jpg b/samples/ControlCatalog/Assets/ModernApp/gallery_alpine.jpg new file mode 100644 index 0000000000..d8b2fce6d8 Binary files /dev/null and b/samples/ControlCatalog/Assets/ModernApp/gallery_alpine.jpg differ diff --git a/samples/ControlCatalog/Assets/ModernApp/gallery_bay.jpg b/samples/ControlCatalog/Assets/ModernApp/gallery_bay.jpg new file mode 100644 index 0000000000..605f072dfe Binary files /dev/null and b/samples/ControlCatalog/Assets/ModernApp/gallery_bay.jpg differ diff --git a/samples/ControlCatalog/Assets/ModernApp/gallery_city.jpg b/samples/ControlCatalog/Assets/ModernApp/gallery_city.jpg new file mode 100644 index 0000000000..788d63cce9 Binary files /dev/null and b/samples/ControlCatalog/Assets/ModernApp/gallery_city.jpg differ diff --git a/samples/ControlCatalog/Assets/ModernApp/gallery_paris.jpg b/samples/ControlCatalog/Assets/ModernApp/gallery_paris.jpg new file mode 100644 index 0000000000..7e04fcdeca Binary files /dev/null and b/samples/ControlCatalog/Assets/ModernApp/gallery_paris.jpg differ diff --git a/samples/ControlCatalog/Assets/ModernApp/gallery_tropical.jpg b/samples/ControlCatalog/Assets/ModernApp/gallery_tropical.jpg new file mode 100644 index 0000000000..a27ef779a5 Binary files /dev/null and b/samples/ControlCatalog/Assets/ModernApp/gallery_tropical.jpg differ diff --git a/samples/ControlCatalog/Assets/ModernApp/gallery_venice.jpg b/samples/ControlCatalog/Assets/ModernApp/gallery_venice.jpg new file mode 100644 index 0000000000..fcc67fa623 Binary files /dev/null and b/samples/ControlCatalog/Assets/ModernApp/gallery_venice.jpg differ diff --git a/samples/ControlCatalog/Assets/ModernApp/story1.jpg b/samples/ControlCatalog/Assets/ModernApp/story1.jpg new file mode 100644 index 0000000000..b75847fb11 Binary files /dev/null and b/samples/ControlCatalog/Assets/ModernApp/story1.jpg differ diff --git a/samples/ControlCatalog/Assets/ModernApp/story2.jpg b/samples/ControlCatalog/Assets/ModernApp/story2.jpg new file mode 100644 index 0000000000..664c7ef7ef Binary files /dev/null and b/samples/ControlCatalog/Assets/ModernApp/story2.jpg differ diff --git a/samples/ControlCatalog/Assets/ModernApp/story3.jpg b/samples/ControlCatalog/Assets/ModernApp/story3.jpg new file mode 100644 index 0000000000..b949cf63c3 Binary files /dev/null and b/samples/ControlCatalog/Assets/ModernApp/story3.jpg differ diff --git a/samples/ControlCatalog/Assets/Movies/cast1.jpg b/samples/ControlCatalog/Assets/Movies/cast1.jpg new file mode 100644 index 0000000000..2eb838400e Binary files /dev/null and b/samples/ControlCatalog/Assets/Movies/cast1.jpg differ diff --git a/samples/ControlCatalog/Assets/Movies/cast2.jpg b/samples/ControlCatalog/Assets/Movies/cast2.jpg new file mode 100644 index 0000000000..42607923c8 Binary files /dev/null and b/samples/ControlCatalog/Assets/Movies/cast2.jpg differ diff --git a/samples/ControlCatalog/Assets/Movies/continue1.jpg b/samples/ControlCatalog/Assets/Movies/continue1.jpg new file mode 100644 index 0000000000..27457bfe79 Binary files /dev/null and b/samples/ControlCatalog/Assets/Movies/continue1.jpg differ diff --git a/samples/ControlCatalog/Assets/Movies/hero.jpg b/samples/ControlCatalog/Assets/Movies/hero.jpg new file mode 100644 index 0000000000..f48e206e2e Binary files /dev/null and b/samples/ControlCatalog/Assets/Movies/hero.jpg differ diff --git a/samples/ControlCatalog/Assets/Movies/morelike1.jpg b/samples/ControlCatalog/Assets/Movies/morelike1.jpg new file mode 100644 index 0000000000..62852e0d8a Binary files /dev/null and b/samples/ControlCatalog/Assets/Movies/morelike1.jpg differ diff --git a/samples/ControlCatalog/Assets/Movies/search1.jpg b/samples/ControlCatalog/Assets/Movies/search1.jpg new file mode 100644 index 0000000000..17cb2fe685 Binary files /dev/null and b/samples/ControlCatalog/Assets/Movies/search1.jpg differ diff --git a/samples/ControlCatalog/Assets/Movies/toprated1.jpg b/samples/ControlCatalog/Assets/Movies/toprated1.jpg new file mode 100644 index 0000000000..f5de43613e Binary files /dev/null and b/samples/ControlCatalog/Assets/Movies/toprated1.jpg differ diff --git a/samples/ControlCatalog/Assets/Movies/toprated2.jpg b/samples/ControlCatalog/Assets/Movies/toprated2.jpg new file mode 100644 index 0000000000..c54cbd5b34 Binary files /dev/null and b/samples/ControlCatalog/Assets/Movies/toprated2.jpg differ diff --git a/samples/ControlCatalog/Assets/Movies/toprated3.jpg b/samples/ControlCatalog/Assets/Movies/toprated3.jpg new file mode 100644 index 0000000000..c78f4d3278 Binary files /dev/null and b/samples/ControlCatalog/Assets/Movies/toprated3.jpg differ diff --git a/samples/ControlCatalog/Assets/Movies/toprated4.jpg b/samples/ControlCatalog/Assets/Movies/toprated4.jpg new file mode 100644 index 0000000000..f70cd0d283 Binary files /dev/null and b/samples/ControlCatalog/Assets/Movies/toprated4.jpg differ diff --git a/samples/ControlCatalog/Assets/Movies/trending1.jpg b/samples/ControlCatalog/Assets/Movies/trending1.jpg new file mode 100644 index 0000000000..b208d69e33 Binary files /dev/null and b/samples/ControlCatalog/Assets/Movies/trending1.jpg differ diff --git a/samples/ControlCatalog/Assets/Movies/trending2.jpg b/samples/ControlCatalog/Assets/Movies/trending2.jpg new file mode 100644 index 0000000000..44fcce2e1b Binary files /dev/null and b/samples/ControlCatalog/Assets/Movies/trending2.jpg differ diff --git a/samples/ControlCatalog/Assets/Pulse/cat_hiit.jpg b/samples/ControlCatalog/Assets/Pulse/cat_hiit.jpg new file mode 100644 index 0000000000..010d1c5162 Binary files /dev/null and b/samples/ControlCatalog/Assets/Pulse/cat_hiit.jpg differ diff --git a/samples/ControlCatalog/Assets/Pulse/cat_strength.jpg b/samples/ControlCatalog/Assets/Pulse/cat_strength.jpg new file mode 100644 index 0000000000..bd63302eee Binary files /dev/null and b/samples/ControlCatalog/Assets/Pulse/cat_strength.jpg differ diff --git a/samples/ControlCatalog/Assets/Pulse/cat_yoga.jpg b/samples/ControlCatalog/Assets/Pulse/cat_yoga.jpg new file mode 100644 index 0000000000..64db513b0b Binary files /dev/null and b/samples/ControlCatalog/Assets/Pulse/cat_yoga.jpg differ diff --git a/samples/ControlCatalog/Assets/Pulse/ex_bench.jpg b/samples/ControlCatalog/Assets/Pulse/ex_bench.jpg new file mode 100644 index 0000000000..a188abfa58 Binary files /dev/null and b/samples/ControlCatalog/Assets/Pulse/ex_bench.jpg differ diff --git a/samples/ControlCatalog/Assets/Pulse/ex_deadlifts.jpg b/samples/ControlCatalog/Assets/Pulse/ex_deadlifts.jpg new file mode 100644 index 0000000000..b6a272f9f5 Binary files /dev/null and b/samples/ControlCatalog/Assets/Pulse/ex_deadlifts.jpg differ diff --git a/samples/ControlCatalog/Assets/Pulse/ex_overhead.jpg b/samples/ControlCatalog/Assets/Pulse/ex_overhead.jpg new file mode 100644 index 0000000000..c516f0d2a8 Binary files /dev/null and b/samples/ControlCatalog/Assets/Pulse/ex_overhead.jpg differ diff --git a/samples/ControlCatalog/Assets/Pulse/ex_pullups.jpg b/samples/ControlCatalog/Assets/Pulse/ex_pullups.jpg new file mode 100644 index 0000000000..9086115000 Binary files /dev/null and b/samples/ControlCatalog/Assets/Pulse/ex_pullups.jpg differ diff --git a/samples/ControlCatalog/Assets/Pulse/ex_squats.jpg b/samples/ControlCatalog/Assets/Pulse/ex_squats.jpg new file mode 100644 index 0000000000..449be4bcb2 Binary files /dev/null and b/samples/ControlCatalog/Assets/Pulse/ex_squats.jpg differ diff --git a/samples/ControlCatalog/Assets/Pulse/profile_avatar.jpg b/samples/ControlCatalog/Assets/Pulse/profile_avatar.jpg new file mode 100644 index 0000000000..0b6b907926 Binary files /dev/null and b/samples/ControlCatalog/Assets/Pulse/profile_avatar.jpg differ diff --git a/samples/ControlCatalog/Assets/Pulse/rec_fullbody.jpg b/samples/ControlCatalog/Assets/Pulse/rec_fullbody.jpg new file mode 100644 index 0000000000..7d79ec36b0 Binary files /dev/null and b/samples/ControlCatalog/Assets/Pulse/rec_fullbody.jpg differ diff --git a/samples/ControlCatalog/Assets/Pulse/rec_mobility.jpg b/samples/ControlCatalog/Assets/Pulse/rec_mobility.jpg new file mode 100644 index 0000000000..be573b35a7 Binary files /dev/null and b/samples/ControlCatalog/Assets/Pulse/rec_mobility.jpg differ diff --git a/samples/ControlCatalog/Assets/Pulse/rec_powercore.jpg b/samples/ControlCatalog/Assets/Pulse/rec_powercore.jpg new file mode 100644 index 0000000000..a72c8e9159 Binary files /dev/null and b/samples/ControlCatalog/Assets/Pulse/rec_powercore.jpg differ diff --git a/samples/ControlCatalog/Assets/Pulse/workout_hero.jpg b/samples/ControlCatalog/Assets/Pulse/workout_hero.jpg new file mode 100644 index 0000000000..d2fb1dfe91 Binary files /dev/null and b/samples/ControlCatalog/Assets/Pulse/workout_hero.jpg differ diff --git a/samples/ControlCatalog/Assets/Restaurant/dish1.jpg b/samples/ControlCatalog/Assets/Restaurant/dish1.jpg new file mode 100644 index 0000000000..fa47be4b8a Binary files /dev/null and b/samples/ControlCatalog/Assets/Restaurant/dish1.jpg differ diff --git a/samples/ControlCatalog/Assets/Restaurant/dish2.jpg b/samples/ControlCatalog/Assets/Restaurant/dish2.jpg new file mode 100644 index 0000000000..bdcbfb656f Binary files /dev/null and b/samples/ControlCatalog/Assets/Restaurant/dish2.jpg differ diff --git a/samples/ControlCatalog/Assets/Restaurant/dish3.jpg b/samples/ControlCatalog/Assets/Restaurant/dish3.jpg new file mode 100644 index 0000000000..9f4f906f01 Binary files /dev/null and b/samples/ControlCatalog/Assets/Restaurant/dish3.jpg differ diff --git a/samples/ControlCatalog/Assets/Restaurant/dish4.jpg b/samples/ControlCatalog/Assets/Restaurant/dish4.jpg new file mode 100644 index 0000000000..2bbaf1db9d Binary files /dev/null and b/samples/ControlCatalog/Assets/Restaurant/dish4.jpg differ diff --git a/samples/ControlCatalog/Assets/Restaurant/featured_dish.jpg b/samples/ControlCatalog/Assets/Restaurant/featured_dish.jpg new file mode 100644 index 0000000000..ad21a354dd Binary files /dev/null and b/samples/ControlCatalog/Assets/Restaurant/featured_dish.jpg differ diff --git a/samples/ControlCatalog/Assets/Restaurant/user_avatar.jpg b/samples/ControlCatalog/Assets/Restaurant/user_avatar.jpg new file mode 100644 index 0000000000..664c7ef7ef Binary files /dev/null and b/samples/ControlCatalog/Assets/Restaurant/user_avatar.jpg differ diff --git a/samples/ControlCatalog/Assets/RetroGaming/cyber_city.jpg b/samples/ControlCatalog/Assets/RetroGaming/cyber_city.jpg new file mode 100644 index 0000000000..e262425fff Binary files /dev/null and b/samples/ControlCatalog/Assets/RetroGaming/cyber_city.jpg differ diff --git a/samples/ControlCatalog/Assets/RetroGaming/dungeon_bit.jpg b/samples/ControlCatalog/Assets/RetroGaming/dungeon_bit.jpg new file mode 100644 index 0000000000..6b8967fc83 Binary files /dev/null and b/samples/ControlCatalog/Assets/RetroGaming/dungeon_bit.jpg differ diff --git a/samples/ControlCatalog/Assets/RetroGaming/forest_spirit.jpg b/samples/ControlCatalog/Assets/RetroGaming/forest_spirit.jpg new file mode 100644 index 0000000000..2192a3a72c Binary files /dev/null and b/samples/ControlCatalog/Assets/RetroGaming/forest_spirit.jpg differ diff --git a/samples/ControlCatalog/Assets/RetroGaming/hero.jpg b/samples/ControlCatalog/Assets/RetroGaming/hero.jpg new file mode 100644 index 0000000000..6a265a5ef7 Binary files /dev/null and b/samples/ControlCatalog/Assets/RetroGaming/hero.jpg differ diff --git a/samples/ControlCatalog/Assets/RetroGaming/neon_ninja.jpg b/samples/ControlCatalog/Assets/RetroGaming/neon_ninja.jpg new file mode 100644 index 0000000000..6a5994ec45 Binary files /dev/null and b/samples/ControlCatalog/Assets/RetroGaming/neon_ninja.jpg differ diff --git a/samples/ControlCatalog/Assets/RetroGaming/neon_racer.jpg b/samples/ControlCatalog/Assets/RetroGaming/neon_racer.jpg new file mode 100644 index 0000000000..68c973ba93 Binary files /dev/null and b/samples/ControlCatalog/Assets/RetroGaming/neon_racer.jpg differ diff --git a/samples/ControlCatalog/Assets/RetroGaming/pixel_quest.jpg b/samples/ControlCatalog/Assets/RetroGaming/pixel_quest.jpg new file mode 100644 index 0000000000..9af86cdbb3 Binary files /dev/null and b/samples/ControlCatalog/Assets/RetroGaming/pixel_quest.jpg differ diff --git a/samples/ControlCatalog/Assets/RetroGaming/space_voids.jpg b/samples/ControlCatalog/Assets/RetroGaming/space_voids.jpg new file mode 100644 index 0000000000..b39a00967f Binary files /dev/null and b/samples/ControlCatalog/Assets/RetroGaming/space_voids.jpg differ diff --git a/samples/ControlCatalog/Assets/Sanctuary/city_bg.jpg b/samples/ControlCatalog/Assets/Sanctuary/city_bg.jpg new file mode 100644 index 0000000000..9a8a4b374f Binary files /dev/null and b/samples/ControlCatalog/Assets/Sanctuary/city_bg.jpg differ diff --git a/samples/ControlCatalog/Assets/Sanctuary/forest_bg.jpg b/samples/ControlCatalog/Assets/Sanctuary/forest_bg.jpg new file mode 100644 index 0000000000..32a360431d Binary files /dev/null and b/samples/ControlCatalog/Assets/Sanctuary/forest_bg.jpg differ diff --git a/samples/ControlCatalog/Assets/Sanctuary/main_arctic_silence.jpg b/samples/ControlCatalog/Assets/Sanctuary/main_arctic_silence.jpg new file mode 100644 index 0000000000..61e7b06212 Binary files /dev/null and b/samples/ControlCatalog/Assets/Sanctuary/main_arctic_silence.jpg differ diff --git a/samples/ControlCatalog/Assets/Sanctuary/main_deep_forest.jpg b/samples/ControlCatalog/Assets/Sanctuary/main_deep_forest.jpg new file mode 100644 index 0000000000..d8576b626f Binary files /dev/null and b/samples/ControlCatalog/Assets/Sanctuary/main_deep_forest.jpg differ diff --git a/samples/ControlCatalog/Assets/Sanctuary/main_desert_sands.jpg b/samples/ControlCatalog/Assets/Sanctuary/main_desert_sands.jpg new file mode 100644 index 0000000000..e4b44477e3 Binary files /dev/null and b/samples/ControlCatalog/Assets/Sanctuary/main_desert_sands.jpg differ diff --git a/samples/ControlCatalog/Assets/Sanctuary/main_hero.jpg b/samples/ControlCatalog/Assets/Sanctuary/main_hero.jpg new file mode 100644 index 0000000000..63dff3f948 Binary files /dev/null and b/samples/ControlCatalog/Assets/Sanctuary/main_hero.jpg differ diff --git a/samples/ControlCatalog/Assets/Sanctuary/mountain_bg.jpg b/samples/ControlCatalog/Assets/Sanctuary/mountain_bg.jpg new file mode 100644 index 0000000000..e9fdd5c0d9 Binary files /dev/null and b/samples/ControlCatalog/Assets/Sanctuary/mountain_bg.jpg differ diff --git a/samples/ControlCatalog/ControlCatalog.csproj b/samples/ControlCatalog/ControlCatalog.csproj index c71e7a93ad..2ce24a4d20 100644 --- a/samples/ControlCatalog/ControlCatalog.csproj +++ b/samples/ControlCatalog/ControlCatalog.csproj @@ -11,8 +11,7 @@ Designer - - + diff --git a/samples/ControlCatalog/DecoratedWindow.xaml b/samples/ControlCatalog/DecoratedWindow.xaml index 997ae54f41..804eebfb40 100644 --- a/samples/ControlCatalog/DecoratedWindow.xaml +++ b/samples/ControlCatalog/DecoratedWindow.xaml @@ -2,7 +2,7 @@ xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" x:Class="ControlCatalog.DecoratedWindow" Title="Avalonia Control Gallery" - SystemDecorations="None" Name="Window"> + WindowDecorations="None" Name="Window"> @@ -43,11 +43,11 @@ Hello world! - + - None - BorderOnly - Full + None + BorderOnly + Full CanResize diff --git a/samples/ControlCatalog/MainView.xaml b/samples/ControlCatalog/MainView.xaml index 803ca52254..adea1b90fc 100644 --- a/samples/ControlCatalog/MainView.xaml +++ b/samples/ControlCatalog/MainView.xaml @@ -49,9 +49,22 @@ - - + + + + + + + + + @@ -67,6 +80,11 @@ + + + @@ -99,6 +117,11 @@ + + + @@ -128,6 +151,11 @@ + + + @@ -140,6 +168,9 @@ + + + @@ -167,6 +198,11 @@ + + + @@ -215,17 +251,19 @@ + SelectionChanged="Decorations_SelectionChanged" + ToolTip.Tip="System Decorations"> - None - BorderOnly - Full + None + BorderOnly + Full + SelectionChanged="ThemeVariants_SelectionChanged" + ToolTip.Tip="Theme Variant"> Default Light @@ -234,7 +272,8 @@ + SelectionChanged="Themes_SelectionChanged" + ToolTip.Tip="Catalog Theme"> Fluent Simple @@ -242,7 +281,8 @@ + SelectionChanged="TransparencyLevels_SelectionChanged" + ToolTip.Tip="Window Transparency Level"> None Transparent @@ -253,7 +293,8 @@ + SelectionChanged="FlowDirection_SelectionChanged" + ToolTip.Tip="Flow Direction"> LeftToRight RightToLeft @@ -261,7 +302,8 @@ + SelectedItem="{Binding WindowState}" + ToolTip.Tip="Window State"/> diff --git a/samples/ControlCatalog/MainView.xaml.cs b/samples/ControlCatalog/MainView.xaml.cs index ae8be8cfc6..82172a8ec4 100644 --- a/samples/ControlCatalog/MainView.xaml.cs +++ b/samples/ControlCatalog/MainView.xaml.cs @@ -45,9 +45,9 @@ namespace ControlCatalog private void Decorations_SelectionChanged(object? sender, SelectionChangedEventArgs e) { - if (VisualRoot is Window window && e.AddedItems.Count > 0 && e.AddedItems[0] is SystemDecorations systemDecorations) + if (TopLevel.GetTopLevel(this) is Window window && e.AddedItems.Count > 0 && e.AddedItems[0] is WindowDecorations systemDecorations) { - window.SystemDecorations = systemDecorations; + window.WindowDecorations = systemDecorations; } } @@ -78,8 +78,8 @@ namespace ControlCatalog { base.OnAttachedToVisualTree(e); - if (VisualRoot is Window window) - Decorations.SelectedIndex = (int)window.SystemDecorations; + if (TopLevel.GetTopLevel(this) is Window window) + Decorations.SelectedIndex = (int)window.WindowDecorations; var insets = TopLevel.GetTopLevel(this)!.InsetsManager; if (insets != null) diff --git a/samples/ControlCatalog/MainWindow.xaml b/samples/ControlCatalog/MainWindow.xaml index ebf6ed9a0f..b466315878 100644 --- a/samples/ControlCatalog/MainWindow.xaml +++ b/samples/ControlCatalog/MainWindow.xaml @@ -8,7 +8,6 @@ xmlns:vm="clr-namespace:ControlCatalog.ViewModels;assembly=ControlCatalog" xmlns:v="using:ControlCatalog.Views" ExtendClientAreaToDecorationsHint="{Binding ExtendClientAreaEnabled}" - ExtendClientAreaChromeHints="{Binding ChromeHints}" ExtendClientAreaTitleBarHeightHint="{Binding TitleBarHeight}" CanResize="{Binding CanResize}" CanMinimize="{Binding CanMinimize}" diff --git a/samples/ControlCatalog/Pages/CarouselDemoPage.xaml b/samples/ControlCatalog/Pages/CarouselDemoPage.xaml new file mode 100644 index 0000000000..df4317fcad --- /dev/null +++ b/samples/ControlCatalog/Pages/CarouselDemoPage.xaml @@ -0,0 +1,11 @@ + + + + + + + diff --git a/samples/ControlCatalog/Pages/CarouselDemoPage.xaml.cs b/samples/ControlCatalog/Pages/CarouselDemoPage.xaml.cs new file mode 100644 index 0000000000..36c9961658 --- /dev/null +++ b/samples/ControlCatalog/Pages/CarouselDemoPage.xaml.cs @@ -0,0 +1,91 @@ +using System; +using Avalonia.Controls; +using Avalonia.Interactivity; + +namespace ControlCatalog.Pages +{ + public partial class CarouselDemoPage : UserControl + { + private static readonly (string Group, string Title, string Description, Func Factory)[] Demos = + { + // Overview + ("Overview", "First Look", + "Basic CarouselPage with three pages and page indicator.", + () => new CarouselPageFirstLookPage()), + + // Populate + ("Populate", "Data Templates", + "Bind CarouselPage to an ObservableCollection, add or remove pages at runtime, and switch the page template.", + () => new CarouselPageDataTemplatePage()), + + // Appearance + ("Appearance", "Customization", + "Switch slide direction between horizontal and vertical with PageSlide. Page indicator dots update on each selection.", + () => new CarouselPageCustomizationPage()), + + // Features + ("Features", "Page Transitions", + "Animate page switches with CrossFade or PageSlide.", + () => new CarouselPageTransitionsPage()), + ("Features", "Programmatic Selection", + "Jump to any page programmatically with SelectedIndex and respond to SelectionChanged events.", + () => new CarouselPageSelectionPage()), + ("Features", "Gesture & Keyboard", + "Swipe left/right to navigate pages. Toggle IsGestureEnabled and IsKeyboardNavigationEnabled.", + () => new CarouselPageGesturePage()), + ("Features", "Events", + "SelectionChanged, NavigatedTo, and NavigatedFrom events. Swipe or navigate to see the live event log.", + () => new CarouselPageEventsPage()), + + // Performance + ("Performance", "Performance Monitor", + "Track page count, live page instances, and managed heap size. Observe how GC reclaims memory after removing pages.", + () => new CarouselPagePerformancePage()), + + // Showcases + ("Showcases", "Sanctuary", + "Travel discovery app with 3 full-screen immersive pages. Each page has a real background photo, gradient overlay, and themed content. Built as a 1:1 replica of a Stitch design.", + () => new SanctuaryShowcasePage()), + ("Showcases", "Care Companion", + "Healthcare onboarding with CarouselPage (3 pages), then a TabbedPage patient dashboard. Skip or complete onboarding to navigate to the dashboard via RemovePage.", + () => new CareCompanionAppPage()), + + // Carousel (ItemsControl) demos + ("Carousel", "Getting Started", + "Basic Carousel with image items and previous/next navigation buttons.", + () => new CarouselGettingStartedPage()), + ("Carousel", "Transitions", + "Configure page transitions: PageSlide, CrossFade, 3D Rotation, or None.", + () => new CarouselTransitionsPage()), + ("Carousel", "Customization", + "Adjust orientation and transition type to tailor the carousel layout.", + () => new CarouselCustomizationPage()), + ("Carousel", "Gestures & Keyboard", + "Navigate items via swipe gesture and arrow keys. Toggle each input mode on and off.", + () => new CarouselGesturesPage()), + ("Carousel", "Vertical Orientation", + "Carousel with Orientation set to Vertical, navigated with Up/Down keys, swipe, or buttons.", + () => new CarouselVerticalPage()), + ("Carousel", "Multi-Item Peek", + "Adjust ViewportFraction to show multiple items simultaneously with adjacent cards peeking.", + () => new CarouselMultiItemPage()), + ("Carousel", "Data Binding", + "Bind Carousel to an ObservableCollection and add, remove, or shuffle items at runtime.", + () => new CarouselDataBindingPage()), + ("Carousel", "Curated Gallery", + "Editorial art gallery app with DrawerPage navigation, hero Carousel with PipsPager dots, and a horizontal peek carousel for collection highlights.", + () => new CarouselGalleryAppPage()), + }; + + public CarouselDemoPage() + { + InitializeComponent(); + Loaded += OnLoaded; + } + + private async void OnLoaded(object? sender, RoutedEventArgs e) + { + await SampleNav.PushAsync(NavigationDemoHelper.CreateGalleryHomePage(SampleNav, Demos), null); + } + } +} diff --git a/samples/ControlCatalog/Pages/CarouselPage.xaml b/samples/ControlCatalog/Pages/CarouselPage.xaml index 352fa32e30..c6e20fec5b 100644 --- a/samples/ControlCatalog/Pages/CarouselPage.xaml +++ b/samples/ControlCatalog/Pages/CarouselPage.xaml @@ -1,44 +1,117 @@ - - An items control that displays its items as pages that fill the control. + + A swipeable items control that can reveal adjacent pages with ViewportFraction. - - - + - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + - - - Transition - + + + + Transition + None - Slide - Crossfade - 3D Rotation + Page Slide + Cross Fade + Rotate 3D + Card Stack + Wave Reveal + Composite (Slide + Fade) - - - Orientation - + Orientation + Horizontal Vertical + + Viewport Fraction + + + + 1.00 + + + + + + + + + Wrap Selection + Swipe Enabled + + + + + + + + Total Items: + 0 + + + Selected Index: + 0 + + diff --git a/samples/ControlCatalog/Pages/CarouselPage.xaml.cs b/samples/ControlCatalog/Pages/CarouselPage.xaml.cs index 713da34051..0a0c973b90 100644 --- a/samples/ControlCatalog/Pages/CarouselPage.xaml.cs +++ b/samples/ControlCatalog/Pages/CarouselPage.xaml.cs @@ -1,6 +1,9 @@ using System; +using Avalonia; using Avalonia.Animation; using Avalonia.Controls; +using Avalonia.Controls.Primitives; +using ControlCatalog.Pages.Transitions; namespace ControlCatalog.Pages { @@ -9,28 +12,137 @@ namespace ControlCatalog.Pages public CarouselPage() { InitializeComponent(); + left.Click += (s, e) => carousel.Previous(); right.Click += (s, e) => carousel.Next(); transition.SelectionChanged += TransitionChanged; orientation.SelectionChanged += TransitionChanged; + viewportFraction.ValueChanged += ViewportFractionChanged; + + wrapSelection.IsChecked = carousel.WrapSelection; + wrapSelection.IsCheckedChanged += (s, e) => + { + carousel.WrapSelection = wrapSelection.IsChecked ?? false; + UpdateButtonState(); + }; + + swipeEnabled.IsChecked = carousel.IsSwipeEnabled; + swipeEnabled.IsCheckedChanged += (s, e) => + { + carousel.IsSwipeEnabled = swipeEnabled.IsChecked ?? false; + }; + + carousel.PropertyChanged += (s, e) => + { + if (e.Property == SelectingItemsControl.SelectedIndexProperty) + { + UpdateButtonState(); + } + else if (e.Property == Carousel.ViewportFractionProperty) + { + UpdateViewportFractionDisplay(); + } + }; + + carousel.ViewportFraction = viewportFraction.Value; + UpdateButtonState(); + UpdateViewportFractionDisplay(); + } + + private void UpdateButtonState() + { + itemsCountIndicator.Text = carousel.ItemCount.ToString(); + selectedIndexIndicator.Text = carousel.SelectedIndex.ToString(); + + var wrap = carousel.WrapSelection; + left.IsEnabled = wrap || carousel.SelectedIndex > 0; + right.IsEnabled = wrap || carousel.SelectedIndex < carousel.ItemCount - 1; + } + + private void ViewportFractionChanged(object? sender, RangeBaseValueChangedEventArgs e) + { + carousel.ViewportFraction = Math.Round(e.NewValue, 2); + UpdateViewportFractionDisplay(); + } + + private void UpdateViewportFractionDisplay() + { + var value = carousel.ViewportFraction; + viewportFractionIndicator.Text = value.ToString("0.00"); + + var pagesInView = 1d / value; + viewportFractionHint.Text = value >= 1d + ? "1.00 shows a single full page." + : $"{pagesInView:0.##} pages fit in view. Try 0.80 for peeking or 0.33 for three full items."; } private void TransitionChanged(object? sender, SelectionChangedEventArgs e) { + var isVertical = orientation.SelectedIndex == 1; + var axis = isVertical ? PageSlide.SlideAxis.Vertical : PageSlide.SlideAxis.Horizontal; + switch (transition.SelectedIndex) { case 0: carousel.PageTransition = null; break; case 1: - carousel.PageTransition = new PageSlide(TimeSpan.FromSeconds(0.25), orientation.SelectedIndex == 0 ? PageSlide.SlideAxis.Horizontal : PageSlide.SlideAxis.Vertical); + carousel.PageTransition = new PageSlide(TimeSpan.FromSeconds(0.25), axis); break; case 2: carousel.PageTransition = new CrossFade(TimeSpan.FromSeconds(0.25)); break; case 3: - carousel.PageTransition = new Rotate3DTransition(TimeSpan.FromSeconds(0.5), orientation.SelectedIndex == 0 ? PageSlide.SlideAxis.Horizontal : PageSlide.SlideAxis.Vertical); + carousel.PageTransition = new Rotate3DTransition(TimeSpan.FromSeconds(0.5), axis); + break; + case 4: + carousel.PageTransition = new CardStackPageTransition(TimeSpan.FromSeconds(0.5), axis); + break; + case 5: + carousel.PageTransition = new WaveRevealPageTransition(TimeSpan.FromSeconds(0.8), axis); break; + case 6: + carousel.PageTransition = new CompositePageTransition + { + PageTransitions = + { + new PageSlide(TimeSpan.FromSeconds(0.25), axis), + new CrossFade(TimeSpan.FromSeconds(0.25)), + } + }; + break; + } + + UpdateLayoutForOrientation(isVertical); + } + + private void UpdateLayoutForOrientation(bool isVertical) + { + if (isVertical) + { + Grid.SetColumn(left, 1); + Grid.SetRow(left, 0); + Grid.SetColumn(right, 1); + Grid.SetRow(right, 2); + + left.Padding = new Thickness(20, 10); + right.Padding = new Thickness(20, 10); + + leftArrow.RenderTransform = new Avalonia.Media.RotateTransform(90); + rightArrow.RenderTransform = new Avalonia.Media.RotateTransform(90); + } + else + { + Grid.SetColumn(left, 0); + Grid.SetRow(left, 1); + Grid.SetColumn(right, 2); + Grid.SetRow(right, 1); + + left.Padding = new Thickness(10, 20); + right.Padding = new Thickness(10, 20); + + leftArrow.RenderTransform = null; + rightArrow.RenderTransform = null; } } } diff --git a/samples/ControlCatalog/Pages/CarouselPage/CareCompanionAppPage.xaml b/samples/ControlCatalog/Pages/CarouselPage/CareCompanionAppPage.xaml new file mode 100644 index 0000000000..a11a1ea6b8 --- /dev/null +++ b/samples/ControlCatalog/Pages/CarouselPage/CareCompanionAppPage.xaml @@ -0,0 +1,98 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/samples/ControlCatalog/Pages/CarouselPage/CareCompanionAppPage.xaml.cs b/samples/ControlCatalog/Pages/CarouselPage/CareCompanionAppPage.xaml.cs new file mode 100644 index 0000000000..f7c87f56a3 --- /dev/null +++ b/samples/ControlCatalog/Pages/CarouselPage/CareCompanionAppPage.xaml.cs @@ -0,0 +1,1068 @@ +using System; +using System.Collections.ObjectModel; +using Avalonia; +using Avalonia.Animation; +using Avalonia.Controls; +using Avalonia.Controls.Presenters; +using Avalonia.Controls.Primitives; +using Avalonia.Input; +using Avalonia.Interactivity; +using Avalonia.Layout; +using Avalonia.Media; +using Avalonia.Styling; +using AvaCarouselPage = Avalonia.Controls.CarouselPage; + +namespace ControlCatalog.Pages; + +public partial class CareCompanionAppPage : UserControl +{ + static readonly Color Primary = Color.Parse("#137fec"); + static readonly Color PrimaryDark = Color.Parse("#0a5bb5"); + static readonly Color PrimaryLight = Color.Parse("#e0f0ff"); + static readonly Color BgLight = Color.Parse("#f6f7f8"); + static readonly Color TextDark = Color.Parse("#111827"); + static readonly Color TextMuted = Color.Parse("#64748b"); + static readonly Color CardBg = Colors.White; + static readonly Color SuccessGreen = Color.Parse("#10b981"); + static readonly Color WarningAmber = Color.Parse("#f59e0b"); + + NavigationPage? _navPage; + AvaCarouselPage? _onboarding; + ScrollViewer? _infoPanel; + + public CareCompanionAppPage() + { + InitializeComponent(); + } + + protected override void OnLoaded(RoutedEventArgs e) + { + base.OnLoaded(e); + _infoPanel = this.FindControl("InfoPanel"); + UpdateInfoVisibility(); + + _navPage = this.FindControl("NavPage"); + if (_navPage == null) return; + + _onboarding = BuildOnboardingCarousel(); + _ = _navPage.PushAsync(_onboarding); + } + + protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change) + { + base.OnPropertyChanged(change); + if (change.Property == BoundsProperty) + UpdateInfoVisibility(); + } + + void UpdateInfoVisibility() + { + if (_infoPanel != null) + _infoPanel.IsVisible = Bounds.Width >= 650; + } + + static TextBlock Txt(string text, double size, FontWeight weight, Color color, + double opacity = 1, TextAlignment align = TextAlignment.Left, + TextWrapping wrap = TextWrapping.NoWrap) + => new TextBlock + { + Text = text, + FontSize = size, + FontWeight = weight, + Foreground = new SolidColorBrush(color), + Opacity = opacity, + TextAlignment = align, + TextWrapping = wrap, + }; + + static Button StyledButton(object content, IBrush bg, IBrush fg, double height, + CornerRadius radius, Thickness margin = default, double fontSize = 14, + FontWeight fontWeight = FontWeight.SemiBold, + IBrush? border = null, Thickness borderThick = default) + { + var btn = new Button + { + Content = content, + Background = bg, + Foreground = fg, + Height = height, + CornerRadius = radius, + Margin = margin, + Padding = new Thickness(16, 0), + HorizontalContentAlignment = HorizontalAlignment.Center, + VerticalContentAlignment = VerticalAlignment.Center, + BorderBrush = border, + BorderThickness = borderThick, + }; + + var over = new Style(x => x.OfType + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + M12 10.9c-.61 0-1.1.49-1.1 1.1s.49 1.1 1.1 1.1c.61 0 1.1-.49 1.1-1.1s-.49-1.1-1.1-1.1zM12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm2.19 12.19L6 18l3.81-8.19L18 6l-3.81 8.19z + + + + + + + + + + + + M12 21.35l-1.45-1.32C5.4 15.36 2 12.28 2 8.5 2 5.42 4.42 3 7.5 3c1.74 0 3.41.81 4.5 2.09C13.09 3.81 14.76 3 16.5 3 19.58 3 22 5.42 22 8.5c0 3.78-3.4 6.86-8.55 11.54L12 21.35z + + + + + + + + + + + + M12 12c2.21 0 4-1.79 4-4s-1.79-4-4-4-4 1.79-4 4 1.79 4 4 4zm0 2c-2.67 0-8 1.34-8 4v2h16v-2c0-2.66-5.33-4-8-4z + + + + + + + + + + + + + + diff --git a/samples/ControlCatalog/Pages/CarouselPage/SanctuaryMainPage.xaml.cs b/samples/ControlCatalog/Pages/CarouselPage/SanctuaryMainPage.xaml.cs new file mode 100644 index 0000000000..05a4097a46 --- /dev/null +++ b/samples/ControlCatalog/Pages/CarouselPage/SanctuaryMainPage.xaml.cs @@ -0,0 +1,11 @@ +using Avalonia.Controls; + +namespace ControlCatalog.Pages; + +public partial class SanctuaryMainPage : UserControl +{ + public SanctuaryMainPage() + { + InitializeComponent(); + } +} diff --git a/samples/ControlCatalog/Pages/CarouselPage/SanctuaryShowcasePage.xaml b/samples/ControlCatalog/Pages/CarouselPage/SanctuaryShowcasePage.xaml new file mode 100644 index 0000000000..50864e5e57 --- /dev/null +++ b/samples/ControlCatalog/Pages/CarouselPage/SanctuaryShowcasePage.xaml @@ -0,0 +1,335 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/samples/ControlCatalog/Pages/CarouselPage/SanctuaryShowcasePage.xaml.cs b/samples/ControlCatalog/Pages/CarouselPage/SanctuaryShowcasePage.xaml.cs new file mode 100644 index 0000000000..be91370691 --- /dev/null +++ b/samples/ControlCatalog/Pages/CarouselPage/SanctuaryShowcasePage.xaml.cs @@ -0,0 +1,70 @@ +using System.Linq; +using Avalonia; +using Avalonia.Controls; +using Avalonia.Interactivity; +using Avalonia.Layout; +using Avalonia.Media; +using Avalonia.VisualTree; + +namespace ControlCatalog.Pages; + +public partial class SanctuaryShowcasePage : UserControl +{ + public SanctuaryShowcasePage() + { + InitializeComponent(); + } + + private void OnPage1CTA(object? sender, RoutedEventArgs e) + { + DemoCarousel.SelectedIndex = 1; + } + + private void OnPage2CTA(object? sender, RoutedEventArgs e) + { + DemoCarousel.SelectedIndex = 2; + } + + private async void OnPage3CTA(object? sender, RoutedEventArgs e) + { + var nav = this.FindAncestorOfType(); + if (nav == null) + return; + + var carouselWrapper = nav.NavigationStack.LastOrDefault(); + + var headerGrid = new Grid { ColumnDefinitions = new ColumnDefinitions("*, Auto") }; + headerGrid.Children.Add(new TextBlock + { + Text = "Sanctuary", + VerticalAlignment = VerticalAlignment.Center + }); + var closeIcon = Geometry.Parse( + "M4.397 4.397a1 1 0 0 1 1.414 0L12 10.585l6.19-6.188a1 1 0 0 1 1.414 1.414L13.413 12l6.19 6.189a1 1 0 0 1-1.414 1.414L12 13.413l-6.189 6.19a1 1 0 0 1-1.414-1.414L10.585 12 4.397 5.811a1 1 0 0 1 0-1.414z"); + var closeBtn = new Button + { + Content = new PathIcon { Data = closeIcon }, + Background = Brushes.Transparent, + BorderThickness = new Thickness(0), + Padding = new Thickness(8, 4), + VerticalAlignment = VerticalAlignment.Center + }; + Grid.SetColumn(closeBtn, 1); + headerGrid.Children.Add(closeBtn); + closeBtn.Click += async (_, _) => await nav.PopAsync(null); + + var mainPage = new ContentPage + { + Header = headerGrid, + Content = new SanctuaryMainPage() + }; + NavigationPage.SetHasBackButton(mainPage, false); + + await nav.PushAsync(mainPage); + + if (carouselWrapper != null) + { + nav.RemovePage(carouselWrapper); + } + } +} diff --git a/samples/ControlCatalog/Pages/ClipboardPage.xaml.cs b/samples/ControlCatalog/Pages/ClipboardPage.xaml.cs index 2b87ceb7b1..4cdde6b824 100644 --- a/samples/ControlCatalog/Pages/ClipboardPage.xaml.cs +++ b/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); diff --git a/samples/ControlCatalog/Pages/CommandBar/CommandBarCustomizationPage.xaml b/samples/ControlCatalog/Pages/CommandBar/CommandBarCustomizationPage.xaml new file mode 100644 index 0000000000..1ea3349129 --- /dev/null +++ b/samples/ControlCatalog/Pages/CommandBar/CommandBarCustomizationPage.xaml @@ -0,0 +1,120 @@ + + + M19,13H13V19H11V13H5V11H11V5H13V11H19V13Z + M15,9H5V5H15M12,19A3,3 0 0,1 9,16A3,3 0 0,1 12,13A3,3 0 0,1 15,16A3,3 0 0,1 12,19M17,3H5C3.89,3 3,3.9 3,5V19A2,2 0 0,0 5,21H19A2,2 0 0,0 21,19V7L17,3Z + M18,16.08C17.24,16.08 16.56,16.38 16.04,16.85L8.91,12.7C8.96,12.47 9,12.24 9,12C9,11.76 8.96,11.53 8.91,11.3L15.96,7.19C16.5,7.69 17.21,8 18,8A3,3 0 0,0 21,5A3,3 0 0,0 18,2A3,3 0 0,0 15,5C15,5.24 15.04,5.47 15.09,5.7L8.04,9.81C7.5,9.31 6.79,9 6,9A3,3 0 0,0 3,12A3,3 0 0,0 6,15C6.79,15 7.5,14.69 8.04,14.19L15.16,18.34C15.11,18.55 15.08,18.77 15.08,19C15.08,20.61 16.39,21.91 18,21.91C19.61,21.91 20.92,20.61 20.92,19C20.92,17.39 19.61,16.08 18,16.08Z + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Customize the CommandBar appearance using Background, Foreground, BorderBrush, and CornerRadius. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/samples/ControlCatalog/Pages/CommandBar/CommandBarCustomizationPage.xaml.cs b/samples/ControlCatalog/Pages/CommandBar/CommandBarCustomizationPage.xaml.cs new file mode 100644 index 0000000000..52c98757f0 --- /dev/null +++ b/samples/ControlCatalog/Pages/CommandBar/CommandBarCustomizationPage.xaml.cs @@ -0,0 +1,90 @@ +using Avalonia.Controls; +using Avalonia.Media; + +namespace ControlCatalog.Pages +{ + public partial class CommandBarCustomizationPage : UserControl + { + public CommandBarCustomizationPage() + { + InitializeComponent(); + } + + private void OnBgPresetChanged(object? sender, SelectionChangedEventArgs e) + { + if (LiveBar == null) + return; + + switch (BgPresetCombo.SelectedIndex) + { + case 1: + LiveBar.Background = new SolidColorBrush(Color.Parse("#0078D4")); + break; + case 2: + LiveBar.Background = new SolidColorBrush(Color.Parse("#1C1C1E")); + break; + case 3: + LiveBar.Background = new LinearGradientBrush + { + StartPoint = new Avalonia.RelativePoint(0, 0, Avalonia.RelativeUnit.Relative), + EndPoint = new Avalonia.RelativePoint(1, 0, Avalonia.RelativeUnit.Relative), + GradientStops = + { + new GradientStop(Color.Parse("#3F51B5"), 0), + new GradientStop(Color.Parse("#E91E63"), 1) + } + }; + break; + case 4: + LiveBar.Background = Brushes.Transparent; + break; + default: + LiveBar.ClearValue(BackgroundProperty); + break; + } + } + + private void OnFgChanged(object? sender, SelectionChangedEventArgs e) + { + if (LiveBar == null) + return; + + switch (FgCombo.SelectedIndex) + { + case 1: + LiveBar.Foreground = Brushes.White; + break; + case 2: + LiveBar.Foreground = Brushes.Black; + break; + default: + LiveBar.ClearValue(ForegroundProperty); + break; + } + } + + private void OnRadiusChanged(object? sender, Avalonia.Controls.Primitives.RangeBaseValueChangedEventArgs e) + { + if (LiveBar == null) + return; + + var r = (int)RadiusSlider.Value; + LiveBar.CornerRadius = new Avalonia.CornerRadius(r); + RadiusLabel.Text = $"{r}"; + } + + private void OnBorderChanged(object? sender, Avalonia.Controls.Primitives.RangeBaseValueChangedEventArgs e) + { + if (LiveBar == null) + return; + + var t = (int)BorderSlider.Value; + LiveBar.BorderThickness = new Avalonia.Thickness(t); + BorderLabel.Text = $"{t}"; + if (t > 0) + LiveBar.BorderBrush = Brushes.Gray; + else + LiveBar.BorderBrush = null; + } + } +} diff --git a/samples/ControlCatalog/Pages/CommandBar/CommandBarDynamicOverflowPage.xaml b/samples/ControlCatalog/Pages/CommandBar/CommandBarDynamicOverflowPage.xaml new file mode 100644 index 0000000000..2f771ab42e --- /dev/null +++ b/samples/ControlCatalog/Pages/CommandBar/CommandBarDynamicOverflowPage.xaml @@ -0,0 +1,75 @@ + + + M19,13H13V19H11V13H5V11H11V5H13V11H19V13Z + M15,9H5V5H15M12,19A3,3 0 0,1 9,16A3,3 0 0,1 12,13A3,3 0 0,1 15,16A3,3 0 0,1 12,19M17,3H5C3.89,3 3,3.9 3,5V19A2,2 0 0,0 5,21H19A2,2 0 0,0 21,19V7L17,3Z + M18,16.08C17.24,16.08 16.56,16.38 16.04,16.85L8.91,12.7C8.96,12.47 9,12.24 9,12C9,11.76 8.96,11.53 8.91,11.3L15.96,7.19C16.5,7.69 17.21,8 18,8A3,3 0 0,0 21,5A3,3 0 0,0 18,2A3,3 0 0,0 15,5C15,5.24 15.04,5.47 15.09,5.7L8.04,9.81C7.5,9.31 6.79,9 6,9A3,3 0 0,0 3,12A3,3 0 0,0 6,15C6.79,15 7.5,14.69 8.04,14.19L15.16,18.34C15.11,18.55 15.08,18.77 15.08,19C15.08,20.61 16.39,21.91 18,21.91C19.61,21.91 20.92,20.61 20.92,19C20.92,17.39 19.61,16.08 18,16.08Z + M15.6,10.79C17.04,10.07 18,8.64 18,7C18,4.79 16.21,3 14,3H7V21H14.73C16.78,21 18.5,19.37 18.5,17.32C18.5,15.82 17.72,14.53 16.5,13.77C16.2,13.59 15.9,13.44 15.6,13.32V10.79M10,6.5H13C13.83,6.5 14.5,7.17 14.5,8C14.5,8.83 13.83,9.5 13,9.5H10V6.5M13.5,17.5H10V14H13.5C14.33,14 15,14.67 15,15.5C15,16.33 14.33,17.5 13.5,17.5Z + M10,4V7H12.21L8.79,15H6V18H14V15H11.79L15.21,7H18V4H10Z + M19,4H15.5L14.5,3H9.5L8.5,4H5V6H19M6,19A2,2 0 0,0 8,21H16A2,2 0 0,0 18,19V7H6V19Z + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/samples/ControlCatalog/Pages/CommandBar/CommandBarDynamicOverflowPage.xaml.cs b/samples/ControlCatalog/Pages/CommandBar/CommandBarDynamicOverflowPage.xaml.cs new file mode 100644 index 0000000000..45a1c95527 --- /dev/null +++ b/samples/ControlCatalog/Pages/CommandBar/CommandBarDynamicOverflowPage.xaml.cs @@ -0,0 +1,45 @@ +using System.Collections.Specialized; +using Avalonia.Controls; +using Avalonia.Interactivity; + +namespace ControlCatalog.Pages +{ + public partial class CommandBarDynamicOverflowPage : UserControl + { + public CommandBarDynamicOverflowPage() + { + InitializeComponent(); + ((INotifyCollectionChanged)DemoBar.OverflowItems).CollectionChanged += OnOverflowChanged; + UpdateStatus(); + } + + private void OnWidthChanged(object? sender, Avalonia.Controls.Primitives.RangeBaseValueChangedEventArgs e) + { + if (BarContainer == null) + return; + var width = (int)WidthSlider.Value; + BarContainer.Width = width; + WidthLabel.Text = $"{width}"; + } + + private void OnDynamicOverflowChanged(object? sender, RoutedEventArgs e) + { + if (DemoBar == null) + return; + DemoBar.IsDynamicOverflowEnabled = DynamicOverflowCheck.IsChecked == true; + } + + private void OnOverflowChanged(object? sender, NotifyCollectionChangedEventArgs e) + { + UpdateStatus(); + } + + private void UpdateStatus() + { + var total = DemoBar.PrimaryCommands.Count; + var overflow = DemoBar.OverflowItems.Count; + var visible = total - overflow; + StatusText.Text = $"Showing {visible} of {total} commands, {overflow} in overflow"; + } + } +} diff --git a/samples/ControlCatalog/Pages/CommandBar/CommandBarEventsPage.xaml b/samples/ControlCatalog/Pages/CommandBar/CommandBarEventsPage.xaml new file mode 100644 index 0000000000..8dbc44e19b --- /dev/null +++ b/samples/ControlCatalog/Pages/CommandBar/CommandBarEventsPage.xaml @@ -0,0 +1,95 @@ + + + M19,13H13V19H11V13H5V11H11V5H13V11H19V13Z + M15,9H5V5H15M12,19A3,3 0 0,1 9,16A3,3 0 0,1 12,13A3,3 0 0,1 15,16A3,3 0 0,1 12,19M17,3H5C3.89,3 3,3.9 3,5V19A2,2 0 0,0 5,21H19A2,2 0 0,0 21,19V7L17,3Z + M18,16.08C17.24,16.08 16.56,16.38 16.04,16.85L8.91,12.7C8.96,12.47 9,12.24 9,12C9,11.76 8.96,11.53 8.91,11.3L15.96,7.19C16.5,7.69 17.21,8 18,8A3,3 0 0,0 21,5A3,3 0 0,0 18,2A3,3 0 0,0 15,5C15,5.24 15.04,5.47 15.09,5.7L8.04,9.81C7.5,9.31 6.79,9 6,9A3,3 0 0,0 3,12A3,3 0 0,0 6,15C6.79,15 7.5,14.69 8.04,14.19L15.16,18.34C15.11,18.55 15.08,18.77 15.08,19C15.08,20.61 16.39,21.91 18,21.91C19.61,21.91 20.92,20.61 20.92,19C20.92,17.39 19.61,16.08 18,16.08Z + M14,2H6A2,2 0 0,0 4,4V20A2,2 0 0,0 6,22H18A2,2 0 0,0 20,20V8L14,2M18,20H6V4H13V9H18V20M16,11V18.1L13.9,16L11.1,18.8L8.3,16L11.1,13.2L9,11.1L16,11Z + M19,4H15.5L14.5,3H9.5L8.5,4H5V6H19M6,19A2,2 0 0,0 8,21H16A2,2 0 0,0 18,19V7H6V19Z + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/samples/ControlCatalog/Pages/DrawerPage/ControlsGalleryAppPage.xaml.cs b/samples/ControlCatalog/Pages/DrawerPage/ControlsGalleryAppPage.xaml.cs new file mode 100644 index 0000000000..6c771f3078 --- /dev/null +++ b/samples/ControlCatalog/Pages/DrawerPage/ControlsGalleryAppPage.xaml.cs @@ -0,0 +1,464 @@ +using System; +using Avalonia; +using Avalonia.Animation; +using Avalonia.Controls; +using Avalonia.Interactivity; +using Avalonia.Layout; +using Avalonia.Media; + +namespace ControlCatalog.Pages; + +public partial class ControlsGalleryAppPage : UserControl +{ + static readonly Color Accent = Color.Parse("#60CDFF"); + static readonly Color ContentBg = Color.Parse("#141414"); + static readonly Color CardBg = Color.Parse("#1F1F1F"); + static readonly Color BorderCol = Color.Parse("#2EFFFFFF"); + static readonly Color TextCol = Color.Parse("#FFFFFF"); + static readonly Color TextSec = Color.Parse("#C8FFFFFF"); + static readonly Color TextMuted = Color.Parse("#80FFFFFF"); + + DrawerPage? _drawer; + NavigationPage? _detailNav; + Button? _selectedBtn; + TextBox? _searchBox; + ContentPage? _preSearchPage; + bool _isSearching; + + public ControlsGalleryAppPage() + { + InitializeComponent(); + + _drawer = this.FindControl("NavDrawer"); + _detailNav = this.FindControl("DetailNav"); + _selectedBtn = this.FindControl + + + + + + + + + + + + + + + + diff --git a/samples/ControlCatalog/Pages/DrawerPage/DrawerPageBreakpointPage.xaml.cs b/samples/ControlCatalog/Pages/DrawerPage/DrawerPageBreakpointPage.xaml.cs new file mode 100644 index 0000000000..0da2eb2ed7 --- /dev/null +++ b/samples/ControlCatalog/Pages/DrawerPage/DrawerPageBreakpointPage.xaml.cs @@ -0,0 +1,84 @@ +using Avalonia.Controls; +using Avalonia.Controls.Primitives; +using Avalonia.Interactivity; + +namespace ControlCatalog.Pages +{ + public partial class DrawerPageBreakpointPage : UserControl + { + private bool _isLoaded; + + public DrawerPageBreakpointPage() + { + InitializeComponent(); + } + + protected override void OnLoaded(RoutedEventArgs e) + { + base.OnLoaded(e); + _isLoaded = true; + DemoDrawer.PropertyChanged += OnDrawerPropertyChanged; + UpdateStatus(); + } + + protected override void OnUnloaded(RoutedEventArgs e) + { + base.OnUnloaded(e); + DemoDrawer.PropertyChanged -= OnDrawerPropertyChanged; + } + + private void OnDrawerPropertyChanged(object? sender, Avalonia.AvaloniaPropertyChangedEventArgs e) + { + if (e.Property == DrawerPage.BoundsProperty) + UpdateStatus(); + } + + private void OnBreakpointChanged(object? sender, RangeBaseValueChangedEventArgs e) + { + if (!_isLoaded) + return; + var value = (int)e.NewValue; + DemoDrawer.DrawerBreakpointLength = value; + BreakpointText.Text = value.ToString(); + UpdateStatus(); + } + + private void OnLayoutChanged(object? sender, SelectionChangedEventArgs e) + { + if (!_isLoaded) + return; + DemoDrawer.DrawerLayoutBehavior = LayoutCombo.SelectedIndex switch + { + 0 => DrawerLayoutBehavior.Split, + 1 => DrawerLayoutBehavior.CompactInline, + 2 => DrawerLayoutBehavior.CompactOverlay, + _ => DrawerLayoutBehavior.Split + }; + UpdateStatus(); + } + + private void OnMenuItemClick(object? sender, RoutedEventArgs e) + { + if (!_isLoaded || sender is not Button button) + return; + var item = button.Tag?.ToString() ?? "Home"; + DetailTitleText.Text = item; + DetailPage.Header = item; + if (DemoDrawer.DrawerLayoutBehavior != DrawerLayoutBehavior.Split) + DemoDrawer.IsOpen = false; + } + + private void UpdateStatus() + { + var isVertical = DemoDrawer.DrawerPlacement == DrawerPlacement.Top || + DemoDrawer.DrawerPlacement == DrawerPlacement.Bottom; + var length = isVertical ? DemoDrawer.Bounds.Height : DemoDrawer.Bounds.Width; + var breakpoint = DemoDrawer.DrawerBreakpointLength; + WidthText.Text = $"{(isVertical ? "Height" : "Width")}: {(int)length} px"; + var isOverlay = breakpoint > 0 && length > 0 && length < breakpoint; + ModeText.Text = isOverlay ? + "Mode: Overlay (below breakpoint)" : + $"Mode: {DemoDrawer.DrawerLayoutBehavior} (above breakpoint)"; + } + } +} diff --git a/samples/ControlCatalog/Pages/DrawerPage/DrawerPageCompactPage.xaml b/samples/ControlCatalog/Pages/DrawerPage/DrawerPageCompactPage.xaml new file mode 100644 index 0000000000..fc34972631 --- /dev/null +++ b/samples/ControlCatalog/Pages/DrawerPage/DrawerPageCompactPage.xaml @@ -0,0 +1,103 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/samples/ControlCatalog/Pages/DrawerPage/DrawerPageCompactPage.xaml.cs b/samples/ControlCatalog/Pages/DrawerPage/DrawerPageCompactPage.xaml.cs new file mode 100644 index 0000000000..b1d84fb5fd --- /dev/null +++ b/samples/ControlCatalog/Pages/DrawerPage/DrawerPageCompactPage.xaml.cs @@ -0,0 +1,78 @@ +using Avalonia.Controls; +using Avalonia.Controls.Primitives; +using Avalonia.Interactivity; + +namespace ControlCatalog.Pages +{ + public partial class DrawerPageCompactPage : UserControl + { + private bool _isLoaded; + + public DrawerPageCompactPage() + { + InitializeComponent(); + } + + protected override void OnLoaded(RoutedEventArgs e) + { + base.OnLoaded(e); + _isLoaded = true; + DemoDrawer.Opened += OnDrawerStatusChanged; + DemoDrawer.Closed += OnDrawerStatusChanged; + } + + protected override void OnUnloaded(RoutedEventArgs e) + { + base.OnUnloaded(e); + DemoDrawer.Opened -= OnDrawerStatusChanged; + DemoDrawer.Closed -= OnDrawerStatusChanged; + } + + private void OnDrawerStatusChanged(object? sender, System.EventArgs e) => UpdateStatus(); + + private void OnLayoutChanged(object? sender, SelectionChangedEventArgs e) + { + if (!_isLoaded) + return; + DemoDrawer.DrawerLayoutBehavior = LayoutCombo.SelectedIndex switch + { + 0 => DrawerLayoutBehavior.CompactOverlay, + 1 => DrawerLayoutBehavior.CompactInline, + _ => DrawerLayoutBehavior.CompactOverlay + }; + } + + private void OnCompactLengthChanged(object? sender, RangeBaseValueChangedEventArgs e) + { + if (!_isLoaded) + return; + DemoDrawer.CompactDrawerLength = e.NewValue; + CompactLengthText.Text = ((int)e.NewValue).ToString(); + } + + private void OnDrawerLengthChanged(object? sender, RangeBaseValueChangedEventArgs e) + { + if (!_isLoaded) + return; + DemoDrawer.DrawerLength = e.NewValue; + DrawerLengthText.Text = ((int)e.NewValue).ToString(); + } + + private void OnMenuItemClick(object? sender, RoutedEventArgs e) + { + if (!_isLoaded) + return; + if (sender is not Button button) + return; + var item = button.Tag?.ToString() ?? "Home"; + DetailTitleText.Text = item; + DetailPage.Header = item; + DemoDrawer.IsOpen = false; + } + + private void UpdateStatus() + { + StatusText.Text = $"Drawer: {(DemoDrawer.IsOpen ? "Open" : "Closed")}"; + } + } +} diff --git a/samples/ControlCatalog/Pages/DrawerPage/DrawerPageCustomFlyoutPage.xaml b/samples/ControlCatalog/Pages/DrawerPage/DrawerPageCustomFlyoutPage.xaml new file mode 100644 index 0000000000..11f584c39d --- /dev/null +++ b/samples/ControlCatalog/Pages/DrawerPage/DrawerPageCustomFlyoutPage.xaml @@ -0,0 +1,268 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/samples/ControlCatalog/Pages/DrawerPage/DrawerPageCustomFlyoutPage.xaml.cs b/samples/ControlCatalog/Pages/DrawerPage/DrawerPageCustomFlyoutPage.xaml.cs new file mode 100644 index 0000000000..c05d92ecf6 --- /dev/null +++ b/samples/ControlCatalog/Pages/DrawerPage/DrawerPageCustomFlyoutPage.xaml.cs @@ -0,0 +1,146 @@ +using System; +using Avalonia.Controls; +using Avalonia.Controls.Shapes; +using Avalonia.Interactivity; +using Avalonia.Media; +using Avalonia.Threading; + +namespace ControlCatalog.Pages +{ + public partial class DrawerPageCustomFlyoutPage : UserControl + { + private Ellipse? _bubble1; + private Ellipse? _bubble2; + private DispatcherTimer? _bubbleTimer; + private double _bubblePhase; + + public DrawerPageCustomFlyoutPage() + { + InitializeComponent(); + + _bubble1 = this.FindControl("Bubble1"); + _bubble2 = this.FindControl("Bubble2"); + + DrawerPageControl.PropertyChanged += (_, args) => + { + if (args.Property == DrawerPage.IsOpenProperty) + OnDrawerOpenChanged((bool)args.NewValue!); + }; + + _ = DetailNav.PushAsync(BuildDetailPage("Home"), null); + } + + private Control[] MenuItems => + new Control[] { MenuItem1, MenuItem2, MenuItem3, MenuItem4, MenuItem5, FooterRow }; + + private void OnDrawerOpenChanged(bool isOpen) + { + if (isOpen) + { + StartBubbles(); + + foreach (var item in MenuItems) + { + item.Opacity = 1.0; + if (item.RenderTransform is TranslateTransform tt) + tt.Y = 0; + } + } + else + { + StopBubbles(); + + foreach (var item in MenuItems) + { + var savedItemT = item.Transitions; + item.Transitions = null; + item.Opacity = 0.0; + item.Transitions = savedItemT; + + if (item.RenderTransform is TranslateTransform tt) + { + var savedTT = tt.Transitions; + tt.Transitions = null; + tt.Y = 25; + tt.Transitions = savedTT; + } + } + } + } + + private void StartBubbles() + { + if (_bubbleTimer != null) return; + _bubblePhase = 0; + _bubbleTimer = new DispatcherTimer(DispatcherPriority.Render) + { + Interval = TimeSpan.FromMilliseconds(16) + }; + _bubbleTimer.Tick += OnBubbleTick; + _bubbleTimer.Start(); + } + + private void StopBubbles() + { + if (_bubbleTimer == null) return; + _bubbleTimer.Stop(); + _bubbleTimer.Tick -= OnBubbleTick; + _bubbleTimer = null; + + if (_bubble1 != null) _bubble1.RenderTransform = null; + if (_bubble2 != null) _bubble2.RenderTransform = null; + } + + private void OnBubbleTick(object? sender, EventArgs e) + { + _bubblePhase += 0.012; + + if (_bubble1 != null) + _bubble1.RenderTransform = new TranslateTransform( + x: Math.Sin(_bubblePhase * 0.65) * 10, + y: Math.Sin(_bubblePhase) * 14); + + if (_bubble2 != null) + _bubble2.RenderTransform = new TranslateTransform( + x: Math.Sin(_bubblePhase * 0.45 + 1.8) * 7, + y: Math.Cos(_bubblePhase * 0.85 + 0.6) * 10); + } + + private async void OnMenuItemClick(object? sender, RoutedEventArgs e) + { + if (sender is not Button button) return; + var tag = button.Tag?.ToString() ?? "Home"; + + DrawerPageControl.IsOpen = false; + + await DetailNav.ReplaceAsync(BuildDetailPage(tag), null); + } + + private static ContentPage BuildDetailPage(string section) + { + var (iconPath, body) = section switch + { + "Home" => + ("M10,20V14H14V20H19V12H22L12,3L2,12H5V20H10Z", + "Welcome back! Here is your dashboard with recent activity, quick actions, and personalized content."), + "Explore" => + ("M12,11.5A2.5,2.5 0 0,1 9.5,9A2.5,2.5 0 0,1 12,6.5A2.5,2.5 0 0,1 14.5,9A2.5,2.5 0 0,1 12,11.5M12,2A7,7 0 0,0 5,9C5,14.25 12,22 12,22C12,22 19,14.25 19,9A7,7 0 0,0 12,2Z", + "Discover new places, trending topics, and recommended content tailored to your interests."), + "Messages" => + ("M20,8L12,13L4,8V6L12,11L20,6M20,4H4C2.89,4 2,4.89 2,6V18A2,2 0 0,0 4,20H20A2,2 0 0,0 22,18V6C22,4.89 21.1,4 20,4Z", + "Your conversations and notifications. Stay connected with the people who matter."), + "Profile" => + ("M12,4A4,4 0 0,1 16,8A4,4 0 0,1 12,12A4,4 0 0,1 8,8A4,4 0 0,1 12,4M12,14C16.42,14 20,15.79 20,18V20H4V18C4,15.79 7.58,14 12,14Z", + "View and edit your profile, manage privacy settings, and control your account preferences."), + "Settings" => + ("M12,15.5A3.5,3.5 0 0,1 8.5,12A3.5,3.5 0 0,1 12,8.5A3.5,3.5 0 0,1 15.5,12A3.5,3.5 0 0,1 12,15.5M19.43,12.97C19.47,12.65 19.5,12.33 19.5,12C19.5,11.67 19.47,11.34 19.43,11L21.54,9.37C21.73,9.22 21.78,8.95 21.66,8.73L19.66,5.27C19.54,5.05 19.27,4.96 19.05,5.05L16.56,6.05C16.04,5.66 15.5,5.32 14.87,5.07L14.5,2.42C14.46,2.18 14.25,2 14,2H10C9.75,2 9.54,2.18 9.5,2.42L9.13,5.07C8.5,5.32 7.96,5.66 7.44,6.05L4.95,5.05C4.73,4.96 4.46,5.05 4.34,5.27L2.34,8.73C2.21,8.95 2.27,9.22 2.46,9.37L4.57,11C4.53,11.34 4.5,11.67 4.5,12C4.5,12.33 4.53,12.65 4.57,12.97L2.46,14.63C2.27,14.78 2.21,15.05 2.34,15.27L4.34,18.73C4.46,18.95 4.73,19.04 4.95,18.95L7.44,17.94C7.96,18.34 8.5,18.68 9.13,18.93L9.5,21.58C9.54,21.82 9.75,22 10,22H14C14.25,22 14.46,21.82 14.5,21.58L14.87,18.93C15.5,18.68 16.04,18.34 16.56,17.94L19.05,18.95C19.27,19.04 19.54,18.95 19.66,18.73L21.66,15.27C21.78,15.05 21.73,14.78 21.54,14.63L19.43,12.97Z", + "Configure application preferences, notifications, and privacy options."), + _ => ("M12,2A10,10 0 0,1 22,12A10,10 0 0,1 12,22A10,10 0 0,1 2,12A10,10 0 0,1 12,2Z", "") + }; + + var page = NavigationDemoHelper.MakeSectionPage(section, iconPath, section, body, 0); + NavigationPage.SetHasNavigationBar(page, false); + return page; + } + } +} diff --git a/samples/ControlCatalog/Pages/DrawerPage/DrawerPageCustomizationPage.xaml b/samples/ControlCatalog/Pages/DrawerPage/DrawerPageCustomizationPage.xaml new file mode 100644 index 0000000000..4987e8979e --- /dev/null +++ b/samples/ControlCatalog/Pages/DrawerPage/DrawerPageCustomizationPage.xaml @@ -0,0 +1,180 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/samples/ControlCatalog/Pages/DrawerPage/DrawerPageCustomizationPage.xaml.cs b/samples/ControlCatalog/Pages/DrawerPage/DrawerPageCustomizationPage.xaml.cs new file mode 100644 index 0000000000..243bc5868b --- /dev/null +++ b/samples/ControlCatalog/Pages/DrawerPage/DrawerPageCustomizationPage.xaml.cs @@ -0,0 +1,205 @@ +using System.Linq; +using Avalonia.Controls; +using Avalonia.Controls.Primitives; +using Avalonia.Input.GestureRecognizers; +using Avalonia.Interactivity; +using Avalonia.Media; + +namespace ControlCatalog.Pages +{ + public partial class DrawerPageCustomizationPage : UserControl + { + private bool _isLoaded; + + private static readonly string[] _iconPaths = + { + // 0 - 3 lines (default hamburger) + "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", + // 1 - 2 lines + "M3,13H21V11H3M3,6V8H21V6", + // 2 - 4 squares + "M3,11H11V3H3M3,21H11V13H3M13,21H21V13H13M13,3V11H21V3", + }; + + public DrawerPageCustomizationPage() + { + InitializeComponent(); + EnableMouseSwipeGesture(DemoDrawer); + } + + protected override void OnLoaded(RoutedEventArgs e) + { + base.OnLoaded(e); + _isLoaded = true; + } + + private void OnToggleDrawer(object? sender, RoutedEventArgs e) + { + if (!_isLoaded) + return; + DemoDrawer.IsOpen = !DemoDrawer.IsOpen; + } + + private void OnBehaviorChanged(object? sender, SelectionChangedEventArgs e) + { + if (!_isLoaded) + return; + DemoDrawer.DrawerBehavior = BehaviorCombo.SelectedIndex switch + { + 0 => DrawerBehavior.Auto, + 1 => DrawerBehavior.Flyout, + 2 => DrawerBehavior.Locked, + 3 => DrawerBehavior.Disabled, + _ => DrawerBehavior.Auto + }; + } + + private void OnLayoutChanged(object? sender, SelectionChangedEventArgs e) + { + if (!_isLoaded) + return; + DemoDrawer.DrawerLayoutBehavior = LayoutCombo.SelectedIndex switch + { + 0 => DrawerLayoutBehavior.Overlay, + 1 => DrawerLayoutBehavior.Split, + 2 => DrawerLayoutBehavior.CompactOverlay, + 3 => DrawerLayoutBehavior.CompactInline, + _ => DrawerLayoutBehavior.Overlay + }; + } + + private void OnPlacementChanged(object? sender, SelectionChangedEventArgs e) + { + if (!_isLoaded) + return; + DemoDrawer.DrawerPlacement = PlacementCombo.SelectedIndex switch + { + 1 => DrawerPlacement.Right, + 2 => DrawerPlacement.Top, + 3 => DrawerPlacement.Bottom, + _ => DrawerPlacement.Left + }; + } + + private void OnGestureToggled(object? sender, RoutedEventArgs e) + { + if (!_isLoaded) + return; + if (sender is CheckBox check) + DemoDrawer.IsGestureEnabled = check.IsChecked == true; + } + + private void OnDrawerLengthChanged(object? sender, RangeBaseValueChangedEventArgs e) + { + if (!_isLoaded) + return; + DemoDrawer.DrawerLength = e.NewValue; + DrawerLengthText.Text = ((int)e.NewValue).ToString(); + } + + private void OnDrawerBgChanged(object? sender, SelectionChangedEventArgs e) + { + if (!_isLoaded) + return; + DemoDrawer.DrawerBackground = DrawerBgCombo.SelectedIndex switch + { + 1 => new SolidColorBrush(Colors.SlateBlue), + 2 => new SolidColorBrush(Colors.DarkCyan), + 3 => new SolidColorBrush(Colors.DarkRed), + 4 => new SolidColorBrush(Colors.DarkGreen), + _ => null + }; + } + + private void OnHeaderBgChanged(object? sender, SelectionChangedEventArgs e) + { + if (!_isLoaded) + return; + DemoDrawer.DrawerHeaderBackground = HeaderBgCombo.SelectedIndex switch + { + 1 => new SolidColorBrush(Colors.DodgerBlue), + 2 => new SolidColorBrush(Colors.Orange), + 3 => new SolidColorBrush(Colors.Teal), + 4 => new SolidColorBrush(Colors.Purple), + _ => null + }; + } + + private void OnFooterBgChanged(object? sender, SelectionChangedEventArgs e) + { + if (!_isLoaded) + return; + DemoDrawer.DrawerFooterBackground = FooterBgCombo.SelectedIndex switch + { + 1 => new SolidColorBrush(Colors.DimGray), + 2 => new SolidColorBrush(Colors.DarkSlateBlue), + 3 => new SolidColorBrush(Colors.DarkOliveGreen), + 4 => new SolidColorBrush(Colors.Maroon), + _ => null + }; + } + + private void OnIconChanged(object? sender, SelectionChangedEventArgs e) + { + if (!_isLoaded) + return; + DemoDrawer.DrawerIcon = Geometry.Parse(_iconPaths[IconCombo.SelectedIndex]); + } + + private void OnBackdropChanged(object? sender, SelectionChangedEventArgs e) + { + if (!_isLoaded) + return; + DemoDrawer.BackdropBrush = BackdropCombo.SelectedIndex switch + { + 1 => new SolidColorBrush(Color.FromArgb(102, 0, 0, 0)), + 2 => new SolidColorBrush(Color.FromArgb(179, 0, 0, 0)), + 3 => new SolidColorBrush(Color.FromArgb(102, 255, 255, 255)), + _ => null + }; + } + + private void OnShowHeaderToggled(object? sender, RoutedEventArgs e) + { + if (!_isLoaded) + return; + if (ShowHeaderCheck.IsChecked == true) + DemoDrawer.DrawerHeader = DrawerHeaderBorder; + else + DemoDrawer.DrawerHeader = null; + } + + private void OnShowFooterToggled(object? sender, RoutedEventArgs e) + { + if (!_isLoaded) + return; + if (ShowFooterCheck.IsChecked == true) + DemoDrawer.DrawerFooter = DrawerFooterBorder; + else + DemoDrawer.DrawerFooter = null; + } + + private void OnMenuItemClick(object? sender, RoutedEventArgs e) + { + if (!_isLoaded) + return; + if (sender is not Button button) return; + var item = button.Tag?.ToString() ?? "Home"; + + DetailTitleText.Text = item; + + if (DemoDrawer.DrawerBehavior != DrawerBehavior.Locked) + DemoDrawer.IsOpen = false; + } + + private static void EnableMouseSwipeGesture(Control control) + { + var recognizer = control.GestureRecognizers + .OfType() + .FirstOrDefault(); + + if (recognizer is not null) + recognizer.IsMouseEnabled = true; + } + } +} diff --git a/samples/ControlCatalog/Pages/DrawerPage/DrawerPageEventsPage.xaml b/samples/ControlCatalog/Pages/DrawerPage/DrawerPageEventsPage.xaml new file mode 100644 index 0000000000..92a2055812 --- /dev/null +++ b/samples/ControlCatalog/Pages/DrawerPage/DrawerPageEventsPage.xaml @@ -0,0 +1,96 @@ + + + + + + + + + + + + + + + + + diff --git a/samples/ControlCatalog/Pages/DrawerPage/DrawerPageEventsPage.xaml.cs b/samples/ControlCatalog/Pages/DrawerPage/DrawerPageEventsPage.xaml.cs new file mode 100644 index 0000000000..02148dc46b --- /dev/null +++ b/samples/ControlCatalog/Pages/DrawerPage/DrawerPageEventsPage.xaml.cs @@ -0,0 +1,129 @@ +using System.Collections.Generic; +using Avalonia.Controls; +using Avalonia.Interactivity; +using Avalonia.Layout; +using Avalonia.Media; + +namespace ControlCatalog.Pages +{ + public partial class DrawerPageEventsPage : UserControl + { + private readonly Dictionary _sectionPages; + + public DrawerPageEventsPage() + { + InitializeComponent(); + + _sectionPages = new Dictionary + { + ["Home"] = CreateSectionPage("Home"), + ["Profile"] = CreateSectionPage("Profile"), + ["Settings"] = CreateSectionPage("Settings"), + }; + + foreach (var (name, page) in _sectionPages) + { + var label = name; + page.NavigatedTo += (_, _) => Log($"{label}: NavigatedTo"); + page.NavigatedFrom += (_, _) => Log($"{label}: NavigatedFrom"); + } + } + + protected override void OnLoaded(RoutedEventArgs e) + { + base.OnLoaded(e); + DemoDrawer.Opened += OnDrawerOpened; + DemoDrawer.Closing += OnClosing; + DemoDrawer.Closed += OnDrawerClosed; + // Set Content here so the initial NavigatedTo events fire + // (VisualRoot is null in the constructor, which suppresses lifecycle events). + DemoDrawer.Content = _sectionPages["Home"]; + } + + protected override void OnUnloaded(RoutedEventArgs e) + { + base.OnUnloaded(e); + DemoDrawer.Opened -= OnDrawerOpened; + DemoDrawer.Closing -= OnClosing; + DemoDrawer.Closed -= OnDrawerClosed; + } + + private void OnDrawerOpened(object? sender, System.EventArgs e) => Log("Opened"); + private void OnDrawerClosed(object? sender, System.EventArgs e) => Log("Closed"); + + private void OnToggle(object? sender, RoutedEventArgs e) + { + DemoDrawer.IsOpen = !DemoDrawer.IsOpen; + } + + private void OnClosing(object? sender, DrawerClosingEventArgs e) + { + if (CancelCheck.IsChecked == true) + { + e.Cancel = true; + CancelCheck.IsChecked = false; + Log("Closing \u2192 cancelled"); + } + else + { + Log("Closing"); + } + } + + private void OnSelectSection(object? sender, RoutedEventArgs e) + { + if (sender is not Button btn) return; + var section = btn.Tag?.ToString() ?? "Home"; + + if (!_sectionPages.TryGetValue(section, out var page)) return; + if (ReferenceEquals(DemoDrawer.Content, page)) + { + DemoDrawer.IsOpen = false; + return; + } + + Log($"\u2192 {section}"); + DemoDrawer.Content = page; + DemoDrawer.IsOpen = false; + } + + private void OnClearLog(object? sender, RoutedEventArgs e) + { + EventLog.Text = string.Empty; + } + + private void Log(string message) + { + EventLog.Text = $"{message}\n{EventLog.Text}"; + } + + private static ContentPage CreateSectionPage(string header) => new ContentPage + { + Header = header, + Content = new StackPanel + { + HorizontalAlignment = HorizontalAlignment.Center, + VerticalAlignment = VerticalAlignment.Center, + Spacing = 8, + Children = + { + new TextBlock + { + Text = header, + FontSize = 24, + FontWeight = FontWeight.SemiBold, + HorizontalAlignment = HorizontalAlignment.Center, + }, + new TextBlock + { + Text = "Tap a drawer item to navigate.\nWatch the event log in the panel.", + TextWrapping = TextWrapping.Wrap, + Opacity = 0.6, + TextAlignment = TextAlignment.Center, + FontSize = 13, + } + } + } + }; + } +} diff --git a/samples/ControlCatalog/Pages/DrawerPage/DrawerPageFirstLookPage.xaml b/samples/ControlCatalog/Pages/DrawerPage/DrawerPageFirstLookPage.xaml new file mode 100644 index 0000000000..e257137ed9 --- /dev/null +++ b/samples/ControlCatalog/Pages/DrawerPage/DrawerPageFirstLookPage.xaml @@ -0,0 +1,66 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/samples/ControlCatalog/Pages/DrawerPage/DrawerPageRtlPage.xaml.cs b/samples/ControlCatalog/Pages/DrawerPage/DrawerPageRtlPage.xaml.cs new file mode 100644 index 0000000000..36454ef8d3 --- /dev/null +++ b/samples/ControlCatalog/Pages/DrawerPage/DrawerPageRtlPage.xaml.cs @@ -0,0 +1,57 @@ +using Avalonia.Controls; +using Avalonia.Interactivity; +using Avalonia.Media; + +namespace ControlCatalog.Pages +{ + public partial class DrawerPageRtlPage : UserControl + { + public DrawerPageRtlPage() + { + InitializeComponent(); + } + + private void OnRtlToggled(object? sender, RoutedEventArgs e) + { + if (DemoDrawer == null) return; + DemoDrawer.FlowDirection = RtlCheckBox.IsChecked == true + ? FlowDirection.RightToLeft + : FlowDirection.LeftToRight; + } + + private void OnPlacementChanged(object? sender, SelectionChangedEventArgs e) + { + if (DemoDrawer == null) return; + DemoDrawer.DrawerPlacement = PlacementCombo.SelectedIndex switch + { + 0 => DrawerPlacement.Left, + 1 => DrawerPlacement.Right, + _ => DrawerPlacement.Left + }; + } + + private void OnToggleDrawer(object? sender, RoutedEventArgs e) + { + if (DemoDrawer == null) return; + DemoDrawer.IsOpen = !DemoDrawer.IsOpen; + } + + private void OnMenuItemClick(object? sender, RoutedEventArgs e) + { + if (sender is not Button button) return; + var item = button.Tag?.ToString() ?? "Home"; + + DetailTitleText.Text = item; + DetailDescriptionText.Text = item switch + { + "Home" => "Toggle RTL to see the drawer flip to the right edge.\nGestures are mirrored: drag from right edge to open, drag right to close.", + "Profile" => "View and edit your profile information here.", + "Messages" => "Your messages and notifications appear here.", + "Settings" => "Configure application preferences and options.", + _ => $"Content for {item}" + }; + + DemoDrawer.IsOpen = false; + } + } +} diff --git a/samples/ControlCatalog/Pages/DrawerPage/DrawerPageTransitionsPage.xaml b/samples/ControlCatalog/Pages/DrawerPage/DrawerPageTransitionsPage.xaml new file mode 100644 index 0000000000..9776163b6a --- /dev/null +++ b/samples/ControlCatalog/Pages/DrawerPage/DrawerPageTransitionsPage.xaml @@ -0,0 +1,118 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/samples/ControlCatalog/Pages/DrawerPage/DrawerPageTransitionsPage.xaml.cs b/samples/ControlCatalog/Pages/DrawerPage/DrawerPageTransitionsPage.xaml.cs new file mode 100644 index 0000000000..23634c1707 --- /dev/null +++ b/samples/ControlCatalog/Pages/DrawerPage/DrawerPageTransitionsPage.xaml.cs @@ -0,0 +1,94 @@ +using System; +using Avalonia; +using Avalonia.Animation; +using Avalonia.Controls; +using Avalonia.Interactivity; +using Avalonia.Media; + +namespace ControlCatalog.Pages +{ + public partial class DrawerPageTransitionsPage : UserControl + { + private string _selectedTransition = "None"; + + public DrawerPageTransitionsPage() + { + InitializeComponent(); + Loaded += OnLoaded; + } + + private async void OnLoaded(object? sender, RoutedEventArgs e) + { + // Null out the default transition — OnTransitionChanged runs during init before DetailNav exists. + DetailNav.PageTransition = null; + await DetailNav.PushAsync(BuildPage("Home", _selectedTransition), null); + } + + private void OnTransitionChanged(object? sender, SelectionChangedEventArgs e) + { + if (DetailNav == null) return; + + _selectedTransition = TransitionCombo.SelectedIndex switch + { + 1 => "CrossFade", + 2 => "PageSlide (H)", + 3 => "PageSlide (V)", + 4 => "Composite (Slide + Fade)", + _ => "None" + }; + + DetailNav.PageTransition = TransitionCombo.SelectedIndex switch + { + 1 => new CrossFade(TimeSpan.FromMilliseconds(300)), + 2 => new PageSlide(TimeSpan.FromMilliseconds(300), PageSlide.SlideAxis.Horizontal), + 3 => new PageSlide(TimeSpan.FromMilliseconds(300), PageSlide.SlideAxis.Vertical), + 4 => new CompositePageTransition + { + PageTransitions = + { + new PageSlide(TimeSpan.FromMilliseconds(300), PageSlide.SlideAxis.Horizontal), + new CrossFade(TimeSpan.FromMilliseconds(300)) + } + }, + _ => null + }; + } + + private async void OnSectionClick(object? sender, RoutedEventArgs e) + { + if (sender is not Button button) return; + var section = button.Tag?.ToString() ?? "Home"; + + DemoDrawer.IsOpen = false; + + await DetailNav.ReplaceAsync(BuildPage(section, _selectedTransition)); + } + + private static ContentPage BuildPage(string section, string transitionName) + { + var (iconPath, body) = section switch + { + "Home" => + ("M10,20V14H14V20H19V12H22L12,3L2,12H5V20H10Z", + "Your dashboard with recent activity, quick actions, and personalized content."), + "Explore" => + ("M12,11.5A2.5,2.5 0 0,1 9.5,9A2.5,2.5 0 0,1 12,6.5A2.5,2.5 0 0,1 14.5,9A2.5,2.5 0 0,1 12,11.5M12,2A7,7 0 0,0 5,9C5,14.25 12,22 12,22C12,22 19,14.25 19,9A7,7 0 0,0 12,2Z", + "Discover new places, trending topics, and recommended content tailored to your interests."), + "Messages" => + ("M20,8L12,13L4,8V6L12,11L20,6M20,4H4C2.89,4 2,4.89 2,6V18A2,2 0 0,0 4,20H20A2,2 0 0,0 22,18V6C22,4.89 21.1,4 20,4Z", + "Your conversations and notifications. Stay connected with the people who matter."), + "Profile" => + ("M12,4A4,4 0 0,1 16,8A4,4 0 0,1 12,12A4,4 0 0,1 8,8A4,4 0 0,1 12,4M12,14C16.42,14 20,15.79 20,18V20H4V18C4,15.79 7.58,14 12,14Z", + "View and edit your profile, manage privacy settings, and control your account preferences."), + "Settings" => + ("M12,15.5A3.5,3.5 0 0,1 8.5,12A3.5,3.5 0 0,1 12,8.5A3.5,3.5 0 0,1 15.5,12A3.5,3.5 0 0,1 12,15.5M19.43,12.97C19.47,12.65 19.5,12.33 19.5,12C19.5,11.67 19.47,11.34 19.43,11L21.54,9.37C21.73,9.22 21.78,8.95 21.66,8.73L19.66,5.27C19.54,5.05 19.27,4.96 19.05,5.05L16.56,6.05C16.04,5.66 15.5,5.32 14.87,5.07L14.5,2.42C14.46,2.18 14.25,2 14,2H10C9.75,2 9.54,2.18 9.5,2.42L9.13,5.07C8.5,5.32 7.96,5.66 7.44,6.05L4.95,5.05C4.73,4.96 4.46,5.05 4.34,5.27L2.34,8.73C2.21,8.95 2.27,9.22 2.46,9.37L4.57,11C4.53,11.34 4.5,11.67 4.5,12C4.5,12.33 4.53,12.65 4.57,12.97L2.46,14.63C2.27,14.78 2.21,15.05 2.34,15.27L4.34,18.73C4.46,18.95 4.73,19.04 4.95,18.95L7.44,17.94C7.96,18.34 8.5,18.68 9.13,18.93L9.5,21.58C9.54,21.82 9.75,22 10,22H14C14.25,22 14.46,21.82 14.5,21.58L14.87,18.93C15.5,18.68 16.04,18.34 16.56,17.94L19.05,18.95C19.27,19.04 19.54,18.95 19.66,18.73L21.66,15.27C21.78,15.05 21.73,14.78 21.54,14.63L19.43,12.97Z", + "Configure application preferences, notifications, and privacy options."), + _ => ("M12,2A10,10 0 0,1 22,12A10,10 0 0,1 12,22A10,10 0 0,1 2,12A10,10 0 0,1 12,2Z", "") + }; + + var page = NavigationDemoHelper.MakeSectionPage(section, iconPath, section, body, 0, $"Transition: {transitionName}"); + NavigationPage.SetHasNavigationBar(page, false); + return page; + } + } +} diff --git a/samples/ControlCatalog/Pages/DrawerPage/EcoTrackerAppPage.xaml b/samples/ControlCatalog/Pages/DrawerPage/EcoTrackerAppPage.xaml new file mode 100644 index 0000000000..1e9106ccfe --- /dev/null +++ b/samples/ControlCatalog/Pages/DrawerPage/EcoTrackerAppPage.xaml @@ -0,0 +1,306 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + M12 3C9 6 6 9 6 13C6 17.4 8.7 21 12 22C15.3 21 18 17.4 18 13C18 9 15 6 12 3Z + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/samples/ControlCatalog/Pages/DrawerPage/EcoTrackerAppPage.xaml.cs b/samples/ControlCatalog/Pages/DrawerPage/EcoTrackerAppPage.xaml.cs new file mode 100644 index 0000000000..49b13b4d90 --- /dev/null +++ b/samples/ControlCatalog/Pages/DrawerPage/EcoTrackerAppPage.xaml.cs @@ -0,0 +1,319 @@ +using System; +using Avalonia; +using Avalonia.Animation; +using Avalonia.Controls; +using Avalonia.Controls.Primitives; +using Avalonia.Input; +using Avalonia.Interactivity; +using Avalonia.Layout; +using Avalonia.Media; + +namespace ControlCatalog.Pages; + +public partial class EcoTrackerAppPage : UserControl +{ + static readonly Color Primary = Color.Parse("#2E7D32"); + static readonly Color Accent = Color.Parse("#4CAF50"); + static readonly Color BgLight = Color.Parse("#F1F8E9"); + static readonly Color TextDark = Color.Parse("#1A2E1C"); + static readonly Color TextMuted = Color.Parse("#90A4AE"); + + const string LeafPath = + "M12 3C9 6 6 9 6 13C6 17.4 8.7 21 12 22C15.3 21 18 17.4 18 13C18 9 15 6 12 3Z"; + + NavigationPage? _navPage; + DrawerPage? _drawerPage; + ScrollViewer? _infoPanel; + Button? _selectedBtn; + + public EcoTrackerAppPage() + { + InitializeComponent(); + + _infoPanel = this.FindControl("InfoPanel"); + _navPage = this.FindControl("NavPage"); + _drawerPage = this.FindControl("DrawerPageControl"); + _selectedBtn = this.FindControl + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/samples/ControlCatalog/Pages/DrawerPage/EcoTrackerHomeView.xaml.cs b/samples/ControlCatalog/Pages/DrawerPage/EcoTrackerHomeView.xaml.cs new file mode 100644 index 0000000000..3f36a4d48b --- /dev/null +++ b/samples/ControlCatalog/Pages/DrawerPage/EcoTrackerHomeView.xaml.cs @@ -0,0 +1,14 @@ +using System; +using Avalonia.Controls; +using Avalonia.Interactivity; + +namespace ControlCatalog.Pages; + +public partial class EcoTrackerHomeView : UserControl +{ + public Action? TreeDetailRequested { get; set; } + + public EcoTrackerHomeView() => InitializeComponent(); + + void OnHeroClick(object? sender, RoutedEventArgs e) => TreeDetailRequested?.Invoke(); +} diff --git a/samples/ControlCatalog/Pages/DrawerPage/EcoTrackerStatsView.xaml b/samples/ControlCatalog/Pages/DrawerPage/EcoTrackerStatsView.xaml new file mode 100644 index 0000000000..82a5e938df --- /dev/null +++ b/samples/ControlCatalog/Pages/DrawerPage/EcoTrackerStatsView.xaml @@ -0,0 +1,224 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/samples/ControlCatalog/Pages/DrawerPage/EcoTrackerStatsView.xaml.cs b/samples/ControlCatalog/Pages/DrawerPage/EcoTrackerStatsView.xaml.cs new file mode 100644 index 0000000000..90aa02066b --- /dev/null +++ b/samples/ControlCatalog/Pages/DrawerPage/EcoTrackerStatsView.xaml.cs @@ -0,0 +1,8 @@ +using Avalonia.Controls; + +namespace ControlCatalog.Pages; + +public partial class EcoTrackerStatsView : UserControl +{ + public EcoTrackerStatsView() => InitializeComponent(); +} diff --git a/samples/ControlCatalog/Pages/DrawerPage/ModernAppPage.xaml b/samples/ControlCatalog/Pages/DrawerPage/ModernAppPage.xaml new file mode 100644 index 0000000000..77a0e3299c --- /dev/null +++ b/samples/ControlCatalog/Pages/DrawerPage/ModernAppPage.xaml @@ -0,0 +1,176 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/samples/ControlCatalog/Pages/DrawerPage/ModernAppPage.xaml.cs b/samples/ControlCatalog/Pages/DrawerPage/ModernAppPage.xaml.cs new file mode 100644 index 0000000000..f0d4d7d622 --- /dev/null +++ b/samples/ControlCatalog/Pages/DrawerPage/ModernAppPage.xaml.cs @@ -0,0 +1,118 @@ +using System.Threading.Tasks; +using Avalonia; +using Avalonia.Controls; +using Avalonia.Interactivity; +using Avalonia.Media; + +namespace ControlCatalog.Pages; + +public partial class ModernAppPage : UserControl +{ + // Palette + static readonly Color Primary = Color.Parse("#0dccf2"); + static readonly Color BgLight = Color.Parse("#f5f8f8"); + + static IBrush BgBrush => new SolidColorBrush(BgLight); + + DrawerPage? _drawerPage; + NavigationPage? _navPage; + ScrollViewer? _infoPanel; + TextBlock? _pageTitle; + Button? _selectedNavBtn; + + public ModernAppPage() + { + InitializeComponent(); + + _infoPanel = this.FindControl("InfoPanel"); + _drawerPage = this.FindControl("DrawerPageControl"); + _navPage = this.FindControl("NavPage"); + _pageTitle = this.FindControl("PageTitle"); + + if (_navPage != null) + NavigateToDiscover(); + } + + protected override void OnLoaded(RoutedEventArgs e) + { + base.OnLoaded(e); + UpdateInfoPanelVisibility(); + } + + protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change) + { + base.OnPropertyChanged(change); + if (change.Property == BoundsProperty) + UpdateInfoPanelVisibility(); + } + + void UpdateInfoPanelVisibility() + { + if (_infoPanel != null) + _infoPanel.IsVisible = Bounds.Width >= 640; + } + + void OnNavClick(object? sender, RoutedEventArgs e) + { + if (sender is not Button btn) return; + SelectNavButton(btn); + _drawerPage!.IsOpen = false; + switch (btn.Tag?.ToString()) + { + case "Discover": NavigateToDiscover(); break; + case "MyTrips": NavigateToMyTrips(); break; + case "Profile": NavigateToProfile(); break; + case "Settings": NavigateToSettings(); break; + } + } + + void OnCloseDrawer(object? sender, RoutedEventArgs e) + { + if (_drawerPage != null) _drawerPage.IsOpen = false; + } + + void SelectNavButton(Button btn) + { + if (_selectedNavBtn != null) + _selectedNavBtn.Background = Brushes.Transparent; + _selectedNavBtn = btn; + btn.Background = new SolidColorBrush(Color.Parse("#1A0dccf2")); + } + + async Task Navigate(ContentPage page) + { + if (_navPage == null) return; + NavigationPage.SetHasBackButton(page, false); + NavigationPage.SetHasNavigationBar(page, false); + await _navPage.PopToRootAsync(); + await _navPage.PushAsync(page); + } + + async void NavigateToDiscover() + { + if (_pageTitle != null) _pageTitle.Text = "Discover"; + SelectNavButton(this.FindControl + + + + + + diff --git a/samples/ControlCatalog/Pages/DrawerPage/ModernProfileView.xaml.cs b/samples/ControlCatalog/Pages/DrawerPage/ModernProfileView.xaml.cs new file mode 100644 index 0000000000..ae2cdb9451 --- /dev/null +++ b/samples/ControlCatalog/Pages/DrawerPage/ModernProfileView.xaml.cs @@ -0,0 +1,8 @@ +using Avalonia.Controls; + +namespace ControlCatalog.Pages; + +public partial class ModernProfileView : UserControl +{ + public ModernProfileView() => InitializeComponent(); +} diff --git a/samples/ControlCatalog/Pages/DrawerPage/ModernSettingsView.xaml b/samples/ControlCatalog/Pages/DrawerPage/ModernSettingsView.xaml new file mode 100644 index 0000000000..a4ee8cd8bb --- /dev/null +++ b/samples/ControlCatalog/Pages/DrawerPage/ModernSettingsView.xaml @@ -0,0 +1,177 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/samples/ControlCatalog/Pages/DrawerPage/ModernSettingsView.xaml.cs b/samples/ControlCatalog/Pages/DrawerPage/ModernSettingsView.xaml.cs new file mode 100644 index 0000000000..d50ade3dda --- /dev/null +++ b/samples/ControlCatalog/Pages/DrawerPage/ModernSettingsView.xaml.cs @@ -0,0 +1,8 @@ +using Avalonia.Controls; + +namespace ControlCatalog.Pages; + +public partial class ModernSettingsView : UserControl +{ + public ModernSettingsView() => InitializeComponent(); +} diff --git a/samples/ControlCatalog/Pages/GesturePage.cs b/samples/ControlCatalog/Pages/GesturePage.cs index c480b512b4..2906091daa 100644 --- a/samples/ControlCatalog/Pages/GesturePage.cs +++ b/samples/ControlCatalog/Pages/GesturePage.cs @@ -52,7 +52,7 @@ namespace ControlCatalog.Pages } }; - RotationGesture.AddHandler(Gestures.PinchEvent, (s, e) => + RotationGesture.AddHandler(InputElement.PinchEvent, (s, e) => { AngleSlider.Value = e.Angle; }); @@ -94,7 +94,7 @@ namespace ControlCatalog.Pages } }; - control.AddHandler(Gestures.PinchEvent, (s, e) => + control.AddHandler(InputElement.PinchEvent, (s, e) => { InitComposition(control!); @@ -114,7 +114,7 @@ namespace ControlCatalog.Pages } }); - control.AddHandler(Gestures.PinchEndedEvent, (s, e) => + control.AddHandler(InputElement.PinchEndedEvent, (s, e) => { InitComposition(control!); @@ -124,7 +124,7 @@ namespace ControlCatalog.Pages } }); - control.AddHandler(Gestures.ScrollGestureEvent, (s, e) => + control.AddHandler(InputElement.ScrollGestureEvent, (s, e) => { InitComposition(control!); @@ -134,8 +134,8 @@ namespace ControlCatalog.Pages var currentSize = control.Bounds.Size * _currentScale; - currentOffset = new Vector3D(MathUtilities.Clamp(currentOffset.X, 0, currentSize.Width - control.Bounds.Width), - (float)MathUtilities.Clamp(currentOffset.Y, 0, currentSize.Height - control.Bounds.Height), + currentOffset = new Vector3D(Math.Clamp(currentOffset.X, 0, currentSize.Width - control.Bounds.Width), + (float)Math.Clamp(currentOffset.Y, 0, currentSize.Height - control.Bounds.Height), 0); compositionVisual.Offset = currentOffset * -1; @@ -171,7 +171,7 @@ namespace ControlCatalog.Pages } }; - control.AddHandler(Gestures.PullGestureEvent, (s, e) => + control.AddHandler(InputElement.PullGestureEvent, (s, e) => { Vector3D center = new((float)control.Bounds.Center.X, (float)control.Bounds.Center.Y, 0); InitComposition(ball!); @@ -183,7 +183,7 @@ namespace ControlCatalog.Pages } }); - control.AddHandler(Gestures.PullGestureEndedEvent, (s, e) => + control.AddHandler(InputElement.PullGestureEndedEvent, (s, e) => { InitComposition(ball!); if (ballCompositionVisual != null) diff --git a/samples/ControlCatalog/Pages/NavigationDemoHelper.cs b/samples/ControlCatalog/Pages/NavigationDemoHelper.cs new file mode 100644 index 0000000000..22e52b6fda --- /dev/null +++ b/samples/ControlCatalog/Pages/NavigationDemoHelper.cs @@ -0,0 +1,242 @@ +using System; +using System.Collections.Generic; +using Avalonia; +using Avalonia.Controls; +using Avalonia.Controls.Primitives; +using Avalonia.Layout; +using Avalonia.Media; + +namespace ControlCatalog.Pages +{ + /// + /// Shared helpers for ControlCatalog demo pages. + /// + internal static class NavigationDemoHelper + { + /// + /// Pastel background brushes cycled by page index. + /// + internal static readonly IBrush[] PageBrushes = + { + new SolidColorBrush(Color.Parse("#BBDEFB")), + new SolidColorBrush(Color.Parse("#C8E6C9")), + new SolidColorBrush(Color.Parse("#FFE0B2")), + new SolidColorBrush(Color.Parse("#E1BEE7")), + new SolidColorBrush(Color.Parse("#FFCDD2")), + new SolidColorBrush(Color.Parse("#B2EBF2")), + }; + + internal static IBrush GetPageBrush(int index) => + PageBrushes[((index % PageBrushes.Length) + PageBrushes.Length) % PageBrushes.Length]; + + /// + /// Creates a simple demo ContentPage with a centered title and subtitle. + /// + internal static ContentPage MakePage(string header, string body, int colorIndex) => + new ContentPage + { + Header = header, + Background = GetPageBrush(colorIndex), + Content = new StackPanel + { + HorizontalAlignment = HorizontalAlignment.Center, + VerticalAlignment = VerticalAlignment.Center, + Spacing = 8, + Children = + { + new TextBlock + { + Text = header, + FontSize = 20, + FontWeight = FontWeight.SemiBold, + HorizontalAlignment = HorizontalAlignment.Center + }, + new TextBlock + { + Text = body, + FontSize = 13, + Opacity = 0.7, + TextWrapping = TextWrapping.Wrap, + TextAlignment = TextAlignment.Center, + MaxWidth = 260 + } + } + }, + HorizontalContentAlignment = HorizontalAlignment.Stretch, + VerticalContentAlignment = VerticalAlignment.Stretch + }; + + /// + /// Creates a demo ContentPage with an icon, title, body, and hint text + /// (used by DrawerPage detail pages). + /// + internal static ContentPage MakeSectionPage( + string header, string iconData, string title, string body, + int colorIndex, string? hint = null) + { + var panel = new StackPanel { Margin = new Thickness(24, 20), Spacing = 12 }; + + panel.Children.Add(new PathIcon + { + Width = 48, + Height = 48, + Data = Geometry.Parse(iconData), + Foreground = new SolidColorBrush(Color.Parse("#0078D4")) + }); + panel.Children.Add(new TextBlock + { + Text = title, + FontSize = 26, + FontWeight = FontWeight.Bold + }); + panel.Children.Add(new TextBlock + { + Text = body, + FontSize = 14, + Opacity = 0.8, + TextWrapping = TextWrapping.Wrap + }); + panel.Children.Add(new Separator { Margin = new Thickness(0, 8) }); + + if (hint != null) + { + panel.Children.Add(new TextBlock + { + Text = hint, + FontSize = 12, + Opacity = 0.45, + FontStyle = FontStyle.Italic, + TextWrapping = TextWrapping.Wrap + }); + } + + return new ContentPage + { + Header = header, + Background = GetPageBrush(colorIndex), + Content = new ScrollViewer { Content = panel }, + HorizontalContentAlignment = HorizontalAlignment.Stretch, + VerticalContentAlignment = VerticalAlignment.Stretch + }; + } + + private static readonly Geometry CloseIcon = Geometry.Parse( + "M4.397 4.397a1 1 0 0 1 1.414 0L12 10.585l6.19-6.188a1 1 0 0 1 1.414 1.414L13.413 12l6.19 6.189a1 1 0 0 1-1.414 1.414L12 13.413l-6.189 6.19a1 1 0 0 1-1.414-1.414L10.585 12 4.397 5.811a1 1 0 0 1 0-1.414z"); + + /// + /// Builds the demo gallery home page for NavigationPage/TabbedPage/DrawerPage demo registries. + /// + internal static ContentPage CreateGalleryHomePage( + NavigationPage nav, + (string Group, string Title, string Description, Func Factory)[] demos) + { + var stack = new StackPanel { Margin = new Thickness(12), Spacing = 16 }; + + var groups = new Dictionary(); + var groupOrder = new List(); + + foreach (var (group, title, description, factory) in demos) + { + if (!groups.ContainsKey(group)) + { + groups[group] = new WrapPanel + { + Orientation = Orientation.Horizontal, + HorizontalAlignment = HorizontalAlignment.Left + }; + groupOrder.Add(group); + } + + var demoFactory = factory; + var demoTitle = title; + + var card = new Button + { + Width = 170, + MinHeight = 80, + Margin = new Thickness(0, 0, 8, 8), + VerticalAlignment = VerticalAlignment.Top, + HorizontalContentAlignment = HorizontalAlignment.Left, + VerticalContentAlignment = VerticalAlignment.Top, + Padding = new Thickness(12, 8), + Content = new StackPanel + { + Spacing = 4, + Children = + { + new TextBlock + { + Text = title, + FontSize = 13, + FontWeight = FontWeight.SemiBold, + TextWrapping = TextWrapping.Wrap + }, + new TextBlock + { + Text = description, + FontSize = 11, + Opacity = 0.6, + TextWrapping = TextWrapping.Wrap + } + } + } + }; + + card.Click += async (_, _) => + { + var headerGrid = new Grid { ColumnDefinitions = new ColumnDefinitions("*, Auto") }; + headerGrid.Children.Add(new TextBlock + { + Text = demoTitle, + VerticalAlignment = VerticalAlignment.Center + }); + var closeBtn = new Button + { + Content = new PathIcon { Data = CloseIcon }, + Background = Brushes.Transparent, + BorderThickness = new Thickness(0), + Padding = new Thickness(8, 4), + VerticalAlignment = VerticalAlignment.Center + }; + Grid.SetColumn(closeBtn, 1); + headerGrid.Children.Add(closeBtn); + closeBtn.Click += async (_, _) => await nav.PopAsync(null); + + var page = new ContentPage + { + Header = headerGrid, + Content = demoFactory(), + HorizontalContentAlignment = HorizontalAlignment.Stretch, + VerticalContentAlignment = VerticalAlignment.Stretch + }; + NavigationPage.SetHasBackButton(page, false); + await nav.PushAsync(page, null); + }; + + groups[group].Children.Add(card); + } + + foreach (var groupName in groupOrder) + { + stack.Children.Add(new TextBlock + { + Text = groupName, + FontSize = 13, + FontWeight = FontWeight.SemiBold, + Margin = new Thickness(0, 0, 0, 4), + Opacity = 0.6 + }); + stack.Children.Add(groups[groupName]); + } + + var homePage = new ContentPage + { + Content = new ScrollViewer { Content = stack }, + HorizontalContentAlignment = HorizontalAlignment.Stretch, + VerticalContentAlignment = VerticalAlignment.Stretch + }; + NavigationPage.SetHasNavigationBar(homePage, false); + return homePage; + } + } +} diff --git a/samples/ControlCatalog/Pages/NavigationDemoPage.xaml b/samples/ControlCatalog/Pages/NavigationDemoPage.xaml new file mode 100644 index 0000000000..4849b2d5b8 --- /dev/null +++ b/samples/ControlCatalog/Pages/NavigationDemoPage.xaml @@ -0,0 +1,11 @@ + + + + + + + diff --git a/samples/ControlCatalog/Pages/NavigationDemoPage.xaml.cs b/samples/ControlCatalog/Pages/NavigationDemoPage.xaml.cs new file mode 100644 index 0000000000..d65a43a6ad --- /dev/null +++ b/samples/ControlCatalog/Pages/NavigationDemoPage.xaml.cs @@ -0,0 +1,94 @@ +using System; +using Avalonia.Controls; +using Avalonia.Interactivity; + +namespace ControlCatalog.Pages +{ + public partial class NavigationDemoPage : UserControl + { + private static readonly (string Group, string Title, string Description, Func Factory)[] Demos = + { + // Overview + ("Overview", "First Look", "Basic NavigationPage with push/pop navigation and back button support.", + () => new NavigationPageFirstLookPage()), + ("Overview", "Modal Navigation", "Push and pop modal pages that appear on top of the navigation stack.", + () => new NavigationPageModalPage()), + ("Overview", "Navigation Events", + "Subscribe to Pushed, Popped, PoppedToRoot, ModalPushed, and ModalPopped events.", + () => new NavigationPageEventsPage()), + + // Appearance + ("Appearance", "Bar Customization", + "Customize the navigation bar background, foreground, shadow, and visibility.", + () => new NavigationPageAppearancePage()), + ("Appearance", "Header", + "Set page header content: a string, icon, or any custom control in the navigation bar.", + () => new NavigationPageTitlePage()), + + // Data + ("Data", "Pass Data", "Pass data during navigation via constructor arguments or DataContext.", + () => new NavigationPagePassDataPage()), + ("Data", "MVVM Navigation", + "Keep navigation decisions in view models by routing NavigationPage push and pop operations through a small INavigationService.", + () => new NavigationPageMvvmPage()), + + // Features + ("Features", "Attached Methods", + "Per-page navigation bar and back button control via static attached methods.", + () => new NavigationPageAttachedMethodsPage()), + ("Features", "Back Button", "Customize, hide, or intercept the back button.", + () => new NavigationPageBackButtonPage()), + ("Features", "CommandBar", + "Add, remove and position CommandBar items inside the navigation bar or as a bottom bar.", + () => new NavigationPageToolbarPage()), + ("Features", "Transitions", + "Configure page transitions: PageSlide, Parallax Slide, CrossFade, Fade Through, and more.", + () => new NavigationPageTransitionsPage()), + ("Features", "Modal Transitions", "Configure modal transition: PageSlide from bottom, CrossFade, or None.", + () => new NavigationPageModalTransitionsPage()), + ("Features", "Stack Management", "Remove or insert pages anywhere in the navigation stack at runtime.", + () => new NavigationPageStackPage()), + ("Features", "Interactive Header", + "Build a header with a title and live search box that filters page content in real time.", + () => new NavigationPageInteractiveHeaderPage()), + ("Features", "Back Swipe Gesture", "Swipe from the left edge to interactively pop the current page.", + () => new NavigationPageGesturePage()), + ("Features", "Scroll-Aware Bar", + "Hide the navigation bar on downward scroll and reveal it on upward scroll.", + () => new NavigationPageScrollAwarePage()), + + // Performance + ("Performance", "Performance Monitor", + "Track stack depth, live page instances, and managed heap size. Observe how memory is reclaimed after popping pages.", + () => new NavigationPagePerformancePage()), + + // Showcases + ("Showcases", "Pulse Fitness", + "Login flow with RemovePage, TabbedPage dashboard with bottom tabs, and NavigationPage push for workout detail.", + () => new PulseAppPage()), + ("Showcases", "L'Avenir", + "Restaurant app with DrawerPage flyout menu, TabbedPage bottom tabs, and NavigationPage push for dish detail.", + () => new LAvenirAppPage()), + ("Showcases", "AvaloniaFlix", + "Streaming app with dark NavigationPage, hidden nav bar on home, and custom bar tint on movie detail pages.", + () => new AvaloniaFlixAppPage()), + ("Showcases", "Retro Gaming", + "Arcade-style app with NavigationPage header, TabbedPage bottom tabs with CenteredTabPanel, and game detail push.", + () => new RetroGamingAppPage()), + ("Showcases", "Curved Header", + "Shop app with dome-bottomed white header on home (nav bar hidden) and blue curved header on detail (BarLayoutBehavior.Overlay).", + () => new NavigationPageCurvedHeaderPage()), + }; + + public NavigationDemoPage() + { + InitializeComponent(); + Loaded += OnLoaded; + } + + private async void OnLoaded(object? sender, RoutedEventArgs e) + { + await SampleNav.PushAsync(NavigationDemoHelper.CreateGalleryHomePage(SampleNav, Demos), null); + } + } +} diff --git a/samples/ControlCatalog/Pages/NavigationPage/AvaloniaFlixAppPage.xaml b/samples/ControlCatalog/Pages/NavigationPage/AvaloniaFlixAppPage.xaml new file mode 100644 index 0000000000..6e209c8bb5 --- /dev/null +++ b/samples/ControlCatalog/Pages/NavigationPage/AvaloniaFlixAppPage.xaml @@ -0,0 +1,258 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/samples/ControlCatalog/Pages/NavigationPage/AvaloniaFlixAppPage.xaml.cs b/samples/ControlCatalog/Pages/NavigationPage/AvaloniaFlixAppPage.xaml.cs new file mode 100644 index 0000000000..a2ae34dd1a --- /dev/null +++ b/samples/ControlCatalog/Pages/NavigationPage/AvaloniaFlixAppPage.xaml.cs @@ -0,0 +1,191 @@ +using System; +using System.Threading.Tasks; +using Avalonia; +using Avalonia.Animation; +using Avalonia.Controls; +using Avalonia.Interactivity; +using Avalonia.Layout; +using Avalonia.Media; + +namespace ControlCatalog.Pages; + +public partial class AvaloniaFlixAppPage : UserControl +{ + NavigationPage? _detailNav; + ScrollViewer? _infoPanel; + + public AvaloniaFlixAppPage() + { + InitializeComponent(); + + _detailNav = this.FindControl("DetailNav"); + if (_detailNav != null) + { + _detailNav.ModalTransition = new PageSlide(TimeSpan.FromMilliseconds(300), PageSlide.SlideAxis.Vertical); + + var homeView = new AvaloniaFlixHomeView(); + homeView.MovieSelected = title => PushDetailPage(title); + homeView.SearchRequested = () => _ = PushSearchPageAsync(); + + var homePage = new ContentPage + { + Content = homeView, + Background = Brushes.Transparent, + Header = BuildHomeHeader(), + }; + NavigationPage.SetTopCommandBar(homePage, BuildHomeCommandBar()); + _ = _detailNav.PushAsync(homePage); + } + } + + protected override void OnLoaded(RoutedEventArgs e) + { + base.OnLoaded(e); + + _infoPanel = this.FindControl("InfoPanel"); + UpdateInfoPanelVisibility(); + } + + protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change) + { + base.OnPropertyChanged(change); + if (change.Property == BoundsProperty) + UpdateInfoPanelVisibility(); + } + + void UpdateInfoPanelVisibility() + { + if (_infoPanel != null) + _infoPanel.IsVisible = Bounds.Width >= 650; + } + + TextBlock BuildHomeHeader() => new TextBlock + { + Text = "AVALONIAFLIX", + Foreground = new SolidColorBrush(Color.Parse("#E50914")), + FontSize = 18, + FontWeight = Avalonia.Media.FontWeight.Black, + VerticalAlignment = VerticalAlignment.Center, + }; + + StackPanel BuildHomeCommandBar() + { + var cmdBar = new StackPanel + { + Orientation = Orientation.Horizontal, + Spacing = 12, + VerticalAlignment = VerticalAlignment.Center, + Margin = new Thickness(0, 0, 8, 0), + }; + var searchBtn = new Button { Padding = new Thickness(4) }; + searchBtn.Classes.Add("flixTransparent"); + searchBtn.Click += OnSearchClick; + searchBtn.Content = new PathIcon + { + Width = 20, Height = 20, Foreground = Brushes.White, + Data = Avalonia.Media.Geometry.Parse("M9.5,3A6.5,6.5 0 0,1 16,9.5C16,11.11 15.41,12.59 14.44,13.73L14.71,14H15.5L20.5,19L19,20.5L14,15.5V14.71L13.73,14.44C12.59,15.41 11.11,16 9.5,16A6.5,6.5 0 0,1 3,9.5A6.5,6.5 0 0,1 9.5,3M9.5,5C7,5 5,7 5,9.5C5,12 7,14 9.5,14C12,14 14,12 14,9.5C14,7 12,5 9.5,5Z"), + }; + cmdBar.Children.Add(searchBtn); + cmdBar.Children.Add(new Border + { + Width = 30, Height = 30, CornerRadius = new CornerRadius(4), + Background = new SolidColorBrush(Color.Parse("#333333")), + Child = new TextBlock + { + Text = "JD", FontSize = 10, FontWeight = Avalonia.Media.FontWeight.Bold, + Foreground = Brushes.White, + HorizontalAlignment = HorizontalAlignment.Center, + VerticalAlignment = VerticalAlignment.Center, + }, + }); + return cmdBar; + } + + async void PushDetailPage(string title) + { + if (_detailNav == null) return; + + var detailView = new AvaloniaFlixDetailView(title); + + var headerTitle = new TextBlock + { + Text = title, FontSize = 17, FontWeight = Avalonia.Media.FontWeight.Bold, + Foreground = Brushes.White, VerticalAlignment = VerticalAlignment.Center, + }; + + var shareBtnContent = new PathIcon + { + Width = 20, Height = 20, Foreground = Brushes.White, + Data = Avalonia.Media.Geometry.Parse("M18,16.08C17.24,16.08 16.56,16.38 16.04,16.85L8.91,12.7C8.96,12.47 9,12.24 9,12C9,11.76 8.96,11.53 8.91,11.3L15.96,7.19C16.5,7.69 17.21,8 18,8A3,3 0 0,0 21,5A3,3 0 0,0 18,2A3,3 0 0,0 15,5C15,5.24 15.04,5.47 15.09,5.7L8.04,9.81C7.5,9.31 6.79,9 6,9A3,3 0 0,0 3,12A3,3 0 0,0 6,15C6.79,15 7.5,14.69 8.04,14.19L15.16,18.35C15.11,18.56 15.08,18.78 15.08,19C15.08,20.61 16.39,21.92 18,21.92C19.61,21.92 20.92,20.61 20.92,19C20.92,17.39 19.61,16.08 18,16.08Z"), + }; + var shareBtn = new Button { Padding = new Thickness(8), Content = shareBtnContent }; + shareBtn.Classes.Add("flixTransparent"); + + var bookmarkBtnContent = new PathIcon + { + Width = 20, Height = 20, Foreground = Brushes.White, + Data = Avalonia.Media.Geometry.Parse("M17,3H7A2,2 0 0,0 5,5V21L12,18L19,21V5C19,3.89 18.1,3 17,3Z"), + }; + var bookmarkBtn = new Button { Padding = new Thickness(8), Content = bookmarkBtnContent }; + bookmarkBtn.Classes.Add("flixTransparent"); + + var detailCmdBar = new StackPanel + { + Orientation = Orientation.Horizontal, Spacing = 8, + VerticalAlignment = VerticalAlignment.Center, + }; + detailCmdBar.Children.Add(shareBtn); + detailCmdBar.Children.Add(bookmarkBtn); + + var detailPage = new ContentPage + { + Content = detailView, + Background = Brushes.Transparent, + Header = headerTitle, + }; + NavigationPage.SetTopCommandBar(detailPage, detailCmdBar); + + await _detailNav.PushAsync(detailPage); + + var drawer = this.FindControl("DrawerPageControl"); + if (drawer is { IsOpen: true }) + drawer.IsOpen = false; + } + + async void OnSearchClick(object? sender, RoutedEventArgs e) + { + await PushSearchPageAsync(); + } + + async Task PushSearchPageAsync() + { + if (_detailNav == null) return; + + var searchView = new AvaloniaFlixSearchView(); + searchView.CloseRequested = async () => await (_detailNav?.PopModalAsync() ?? Task.CompletedTask); + searchView.MovieSelected = async title => + { + if (_detailNav != null && _detailNav.ModalStack.Count > 0) + await _detailNav.PopModalAsync(); + PushDetailPage(title); + }; + + var searchPage = new ContentPage + { + Content = searchView, + Background = new SolidColorBrush(Color.Parse("#0A0A0A")), + }; + NavigationPage.SetHasNavigationBar(searchPage, false); + + await (_detailNav?.PushModalAsync(searchPage) ?? Task.CompletedTask); + } + + void OnMenuItemClick(object? sender, RoutedEventArgs e) + { + var drawer = this.FindControl("DrawerPageControl"); + if (drawer != null) + drawer.IsOpen = false; + + _ = _detailNav?.PopToRootAsync(); + } +} diff --git a/samples/ControlCatalog/Pages/NavigationPage/AvaloniaFlixDetailView.xaml b/samples/ControlCatalog/Pages/NavigationPage/AvaloniaFlixDetailView.xaml new file mode 100644 index 0000000000..e6f530669c --- /dev/null +++ b/samples/ControlCatalog/Pages/NavigationPage/AvaloniaFlixDetailView.xaml @@ -0,0 +1,332 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/samples/ControlCatalog/Pages/NavigationPage/AvaloniaFlixDetailView.xaml.cs b/samples/ControlCatalog/Pages/NavigationPage/AvaloniaFlixDetailView.xaml.cs new file mode 100644 index 0000000000..659fea52e0 --- /dev/null +++ b/samples/ControlCatalog/Pages/NavigationPage/AvaloniaFlixDetailView.xaml.cs @@ -0,0 +1,60 @@ +using System; +using Avalonia.Controls; +using Avalonia.Media; +using Avalonia.Media.Imaging; +using Avalonia.Platform; + +namespace ControlCatalog.Pages; + +public partial class AvaloniaFlixDetailView : UserControl +{ + static readonly string[] MovieAssets = + { + "avares://ControlCatalog/Assets/Movies/trending1.jpg", + "avares://ControlCatalog/Assets/Movies/trending2.jpg", + "avares://ControlCatalog/Assets/Movies/toprated1.jpg", + "avares://ControlCatalog/Assets/Movies/toprated2.jpg", + "avares://ControlCatalog/Assets/Movies/toprated3.jpg", + "avares://ControlCatalog/Assets/Movies/toprated4.jpg", + "avares://ControlCatalog/Assets/Movies/continue1.jpg", + "avares://ControlCatalog/Assets/Movies/morelike1.jpg", + "avares://ControlCatalog/Assets/Movies/search1.jpg", + "avares://ControlCatalog/Assets/Movies/hero.jpg", + "avares://ControlCatalog/Assets/Movies/cast1.jpg", + "avares://ControlCatalog/Assets/Movies/cast2.jpg", + }; + + public AvaloniaFlixDetailView() => InitializeComponent(); + + public AvaloniaFlixDetailView(string movieTitle) + { + InitializeComponent(); + + HeroTitleLabel.Text = movieTitle; + + var rng = new Random(movieTitle.GetHashCode()); + int imgIdx = Math.Abs(movieTitle.GetHashCode()) % MovieAssets.Length; + + string year = (2020 + rng.Next(6)).ToString(); + string rating = $"{6.5 + rng.NextDouble() * 3.0:F1}/10"; + int mins = 90 + rng.Next(60); + string duration = $"{mins / 60}h {mins % 60}m"; + + YearLabel.Text = year; + RatingLabel.Text = rating; + DurationLabel.Text = duration; + + try + { + var uri = new Uri(MovieAssets[imgIdx]); + HeroBg.Background = new ImageBrush(new Bitmap(AssetLoader.Open(uri))) + { + Stretch = Stretch.UniformToFill, + }; + } + catch + { + HeroBg.Background = new SolidColorBrush(Color.Parse("#111111")); + } + } +} diff --git a/samples/ControlCatalog/Pages/NavigationPage/AvaloniaFlixHomeView.xaml b/samples/ControlCatalog/Pages/NavigationPage/AvaloniaFlixHomeView.xaml new file mode 100644 index 0000000000..4f186e910f --- /dev/null +++ b/samples/ControlCatalog/Pages/NavigationPage/AvaloniaFlixHomeView.xaml @@ -0,0 +1,630 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/samples/ControlCatalog/Pages/NavigationPage/AvaloniaFlixHomeView.xaml.cs b/samples/ControlCatalog/Pages/NavigationPage/AvaloniaFlixHomeView.xaml.cs new file mode 100644 index 0000000000..81702bf695 --- /dev/null +++ b/samples/ControlCatalog/Pages/NavigationPage/AvaloniaFlixHomeView.xaml.cs @@ -0,0 +1,21 @@ +using System; +using Avalonia.Controls; +using Avalonia.Interactivity; + +namespace ControlCatalog.Pages; + +public partial class AvaloniaFlixHomeView : UserControl +{ + public Action? MovieSelected { get; set; } + public Action? SearchRequested { get; set; } + + public AvaloniaFlixHomeView() => InitializeComponent(); + + void OnMovieClick(object? sender, RoutedEventArgs e) + { + string title = "Cyber Dune"; + if (sender is Button btn && btn.Tag is string tag) + title = tag; + MovieSelected?.Invoke(title); + } +} diff --git a/samples/ControlCatalog/Pages/NavigationPage/AvaloniaFlixSearchView.xaml b/samples/ControlCatalog/Pages/NavigationPage/AvaloniaFlixSearchView.xaml new file mode 100644 index 0000000000..d4c536521f --- /dev/null +++ b/samples/ControlCatalog/Pages/NavigationPage/AvaloniaFlixSearchView.xaml @@ -0,0 +1,302 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/samples/ControlCatalog/Pages/NavigationPage/AvaloniaFlixSearchView.xaml.cs b/samples/ControlCatalog/Pages/NavigationPage/AvaloniaFlixSearchView.xaml.cs new file mode 100644 index 0000000000..aac3069bed --- /dev/null +++ b/samples/ControlCatalog/Pages/NavigationPage/AvaloniaFlixSearchView.xaml.cs @@ -0,0 +1,36 @@ +using System; +using System.Threading.Tasks; +using Avalonia.Controls; +using Avalonia.Interactivity; +using Avalonia.VisualTree; + +namespace ControlCatalog.Pages; + +public partial class AvaloniaFlixSearchView : UserControl +{ + public Action? CloseRequested { get; set; } + public Action? MovieSelected { get; set; } + + public AvaloniaFlixSearchView() => InitializeComponent(); + + void OnCloseClick(object? sender, RoutedEventArgs e) + { + if (CloseRequested != null) + { + CloseRequested(); + } + else + { + var nav = this.FindAncestorOfType(); + _ = nav?.PopModalAsync() ?? Task.CompletedTask; + } + } + + void OnMovieClick(object? sender, RoutedEventArgs e) + { + string title = "Neon Horizon"; + if (sender is Button btn && btn.Tag is string tag) + title = tag; + MovieSelected?.Invoke(title); + } +} diff --git a/samples/ControlCatalog/Pages/NavigationPage/CurvedHeaderHomeScrollView.xaml b/samples/ControlCatalog/Pages/NavigationPage/CurvedHeaderHomeScrollView.xaml new file mode 100644 index 0000000000..8810c5777b --- /dev/null +++ b/samples/ControlCatalog/Pages/NavigationPage/CurvedHeaderHomeScrollView.xaml @@ -0,0 +1,205 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/samples/ControlCatalog/Pages/NavigationPage/CurvedHeaderHomeScrollView.xaml.cs b/samples/ControlCatalog/Pages/NavigationPage/CurvedHeaderHomeScrollView.xaml.cs new file mode 100644 index 0000000000..c8910584bc --- /dev/null +++ b/samples/ControlCatalog/Pages/NavigationPage/CurvedHeaderHomeScrollView.xaml.cs @@ -0,0 +1,16 @@ +using System; +using Avalonia.Controls; +using Avalonia.Interactivity; + +namespace ControlCatalog.Pages; + +public partial class CurvedHeaderHomeScrollView : UserControl +{ + public Action? NavigateRequested { get; set; } + + public CurvedHeaderHomeScrollView() => InitializeComponent(); + + void OnShopNowClick(object? sender, RoutedEventArgs e) => NavigateRequested?.Invoke(); + + void OnProductClick(object? sender, RoutedEventArgs e) => NavigateRequested?.Invoke(); +} diff --git a/samples/ControlCatalog/Pages/NavigationPage/CurvedHeaderProfileScrollView.xaml b/samples/ControlCatalog/Pages/NavigationPage/CurvedHeaderProfileScrollView.xaml new file mode 100644 index 0000000000..ecfeb1bb78 --- /dev/null +++ b/samples/ControlCatalog/Pages/NavigationPage/CurvedHeaderProfileScrollView.xaml @@ -0,0 +1,104 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/samples/ControlCatalog/Pages/NavigationPage/LAvenirAppPage.xaml.cs b/samples/ControlCatalog/Pages/NavigationPage/LAvenirAppPage.xaml.cs new file mode 100644 index 0000000000..beb0b2dccb --- /dev/null +++ b/samples/ControlCatalog/Pages/NavigationPage/LAvenirAppPage.xaml.cs @@ -0,0 +1,262 @@ +using System; +using System.Collections.ObjectModel; +using System.Threading.Tasks; +using Avalonia; +using Avalonia.Animation; +using Avalonia.Controls; +using Avalonia.Controls.Presenters; +using Avalonia.Controls.Shapes; +using Avalonia.Controls.Templates; +using Avalonia.Interactivity; +using Avalonia.Layout; +using Avalonia.Media; +using Avalonia.Styling; + +namespace ControlCatalog.Pages; + +public partial class LAvenirAppPage : UserControl +{ + static readonly Color Primary = Color.Parse("#4b2bee"); + static readonly Color BgDark = Color.Parse("#131022"); + static readonly Color BgLight = Color.Parse("#f6f6f8"); + static readonly Color TextDark = Color.Parse("#1e293b"); + static readonly Color TextMuted = Color.Parse("#94a3b8"); + static readonly Color BorderLight = Color.Parse("#e2e8f0"); + + NavigationPage? _navPage; + DrawerPage? _drawerPage; + ScrollViewer? _infoPanel; + + public LAvenirAppPage() + { + InitializeComponent(); + + _navPage = this.FindControl("NavPage"); + _drawerPage = this.FindControl("DrawerPageControl"); + + if (_navPage != null) + _ = _navPage.PushAsync(BuildMenuTabbedPage()); + } + + protected override void OnLoaded(RoutedEventArgs e) + { + base.OnLoaded(e); + + _infoPanel = this.FindControl("InfoPanel"); + UpdateInfoPanelVisibility(); + } + + protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change) + { + base.OnPropertyChanged(change); + if (change.Property == BoundsProperty) + UpdateInfoPanelVisibility(); + } + + void UpdateInfoPanelVisibility() + { + if (_infoPanel != null) + _infoPanel.IsVisible = Bounds.Width >= 650; + } + + void ApplyRootNavigationBarAppearance() + { + if (_navPage == null) + return; + + _navPage.Background = new SolidColorBrush(BgLight); + _navPage.Resources["NavigationBarBackground"] = new SolidColorBrush(BgLight); + _navPage.Resources["NavigationBarForeground"] = new SolidColorBrush(TextDark); + } + + void ApplyDetailNavigationBarAppearance() + { + if (_navPage == null) + return; + + _navPage.Background = new SolidColorBrush(BgDark); + _navPage.Resources["NavigationBarBackground"] = new SolidColorBrush(BgDark); + _navPage.Resources["NavigationBarForeground"] = Brushes.White; + } + + TabbedPage BuildMenuTabbedPage() + { + var tp = new TabbedPage + { + Background = new SolidColorBrush(BgLight), + TabPlacement = TabPlacement.Bottom, + PageTransition = new PageSlide(TimeSpan.FromMilliseconds(200)), + }; + tp.Resources["TabItemHeaderFontSize"] = 12.0; + tp.Resources["TabbedPageTabStripBackground"] = Brushes.White; + tp.Resources["TabbedPageTabStripBorderThickness"] = new Thickness(0, 1, 0, 0); + tp.Resources["TabbedPageTabStripBorderBrush"] = new SolidColorBrush(BorderLight); + tp.Resources["TabbedPageTabItemHeaderForegroundSelected"] = new SolidColorBrush(Primary); + tp.Resources["TabbedPageTabItemHeaderForegroundUnselected"] = new SolidColorBrush(TextMuted); + + tp.IndicatorTemplate = new FuncDataTemplate((_, _) => + new Ellipse + { + Width = 5, Height = 5, + Margin = new Thickness(0, 10, 0, 0), + HorizontalAlignment = HorizontalAlignment.Center, + Fill = new SolidColorBrush(Primary), + }); + + tp.Header = new TextBlock + { + Text = "L'Avenir", + FontSize = 18, + FontWeight = FontWeight.Bold, + Foreground = new SolidColorBrush(TextDark), + VerticalAlignment = VerticalAlignment.Center, + TextAlignment = TextAlignment.Center, + }; + ApplyRootNavigationBarAppearance(); + + NavigationPage.SetTopCommandBar(tp, new Button + { + Width = 40, + Height = 40, + CornerRadius = new CornerRadius(12), + Background = Brushes.Transparent, + Foreground = new SolidColorBrush(TextDark), + Padding = new Thickness(8), + BorderThickness = new Thickness(0), + Content = new PathIcon + { + Data = Geometry.Parse("M9.5,3A6.5,6.5 0 0,1 16,9.5C16,11.11 15.41,12.59 14.44,13.73L14.71,14H15.5L20.5,19L19,20.5L14,15.5V14.71L13.73,14.44C12.59,15.41 11.11,16 9.5,16A6.5,6.5 0 0,1 3,9.5A6.5,6.5 0 0,1 9.5,3M9.5,5C7,5 5,7 5,9.5C5,12 7,14 9.5,14C12,14 14,12 14,9.5C14,7 12,5 9.5,5Z"), + Width = 18, + Height = 18, + }, + VerticalAlignment = VerticalAlignment.Center, + }); + + var menuView = new LAvenirMenuView(); + menuView.DishSelected = PushDishDetail; + + var menuPage = new ContentPage + { + Content = menuView, + Background = new SolidColorBrush(BgLight), + Header = "Menu", + Icon = Geometry.Parse("M11 9H9V2H7v7H5V2H3v7c0 2.12 1.66 3.84 3.75 3.97V22h2.5v-9.03C11.34 12.84 13 11.12 13 9V2h-2v7zm5-3v8h2.5v8H21V2c-2.76 0-5 2.24-5 4z"), + }; + + var reservationsPage = new ContentPage + { + Content = new LAvenirReservationsView(), + Background = new SolidColorBrush(BgLight), + Header = "Reservations", + Icon = Geometry.Parse("M19 3h-1V1h-2v2H8V1H6v2H5c-1.11 0-2 .9-2 2v14c0 1.1.89 2 2 2h14c1.1 0 2-.9 2-2V5c0-1.1-.9-2-2-2zm0 16H5V8h14v11zM9 10H7v2h2v-2zm4 0h-2v2h2v-2zm4 0h-2v2h2v-2z"), + }; + + var profilePage = new ContentPage + { + Content = new LAvenirProfileView(), + Background = new SolidColorBrush(BgLight), + Header = "Profile", + Icon = Geometry.Parse("M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm0 3c1.66 0 3 1.34 3 3s-1.34 3-3 3-3-1.34-3-3 1.34-3 3-3zm0 14.2c-2.5 0-4.71-1.28-6-3.22.03-1.99 4-3.08 6-3.08 1.99 0 5.97 1.09 6 3.08-1.29 1.94-3.5 3.22-6 3.22z"), + }; + + tp.Pages = new ObservableCollection { menuPage, reservationsPage, profilePage }; + return tp; + } + + async void PushDishDetail(string name, string price, string description, string imageFile) + { + if (_navPage == null) + return; + + var detail = new ContentPage + { + Content = new LAvenirDishDetailView(name, price, description, imageFile), + Background = new SolidColorBrush(BgDark), + Header = name, + }; + NavigationPage.SetBottomCommandBar(detail, BuildFloatingBar(price)); + detail.Navigating += args => + { + if (args.NavigationType == NavigationType.Pop) + ApplyRootNavigationBarAppearance(); + + return Task.CompletedTask; + }; + + ApplyDetailNavigationBarAppearance(); + await _navPage.PushAsync(detail); + + if (!ReferenceEquals(_navPage.CurrentPage, detail)) + ApplyRootNavigationBarAppearance(); + } + + Border BuildFloatingBar(string price) + { + var bar = new Border + { + CornerRadius = new CornerRadius(16), + Background = new SolidColorBrush(Color.FromArgb(178, BgDark.R, BgDark.G, BgDark.B)), + BorderBrush = new SolidColorBrush(Color.FromArgb(51, 255, 255, 255)), + BorderThickness = new Thickness(1), + Padding = new Thickness(16, 12), + Margin = new Thickness(16, 8, 16, 8), + }; + + var barGrid = new Grid { ColumnDefinitions = new ColumnDefinitions("*,Auto") }; + + var info = new StackPanel { VerticalAlignment = VerticalAlignment.Center }; + info.Children.Add(new TextBlock + { + Text = "Add to Order", + FontSize = 14, + FontWeight = FontWeight.Bold, + Foreground = Brushes.White, + }); + info.Children.Add(new TextBlock + { + Text = price, + FontSize = 12, + FontWeight = FontWeight.Medium, + Foreground = new SolidColorBrush(TextMuted), + }); + barGrid.Children.Add(info); + + var addBtn = new Button + { + Content = "Add", + Width = 80, + Height = 40, + CornerRadius = new CornerRadius(10), + Background = new SolidColorBrush(Primary), + Foreground = Brushes.White, + FontWeight = FontWeight.Bold, + FontSize = 14, + HorizontalContentAlignment = HorizontalAlignment.Center, + VerticalContentAlignment = VerticalAlignment.Center, + }; + var hoverStyle = new Style(x => x.OfType + + + + diff --git a/samples/ControlCatalog/Pages/NavigationPage/LAvenirReservationsView.xaml.cs b/samples/ControlCatalog/Pages/NavigationPage/LAvenirReservationsView.xaml.cs new file mode 100644 index 0000000000..5ea6d72cf8 --- /dev/null +++ b/samples/ControlCatalog/Pages/NavigationPage/LAvenirReservationsView.xaml.cs @@ -0,0 +1,8 @@ +using Avalonia.Controls; + +namespace ControlCatalog.Pages; + +public partial class LAvenirReservationsView : UserControl +{ + public LAvenirReservationsView() => InitializeComponent(); +} diff --git a/samples/ControlCatalog/Pages/NavigationPage/NavigationPageAppearancePage.xaml b/samples/ControlCatalog/Pages/NavigationPage/NavigationPageAppearancePage.xaml new file mode 100644 index 0000000000..f519b418d7 --- /dev/null +++ b/samples/ControlCatalog/Pages/NavigationPage/NavigationPageAppearancePage.xaml @@ -0,0 +1,149 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/samples/ControlCatalog/Pages/NavigationPage/PulseHomeView.xaml.cs b/samples/ControlCatalog/Pages/NavigationPage/PulseHomeView.xaml.cs new file mode 100644 index 0000000000..5f98cee05f --- /dev/null +++ b/samples/ControlCatalog/Pages/NavigationPage/PulseHomeView.xaml.cs @@ -0,0 +1,25 @@ +using System; +using Avalonia.Controls; +using Avalonia.Input; +using Avalonia.Interactivity; + +namespace ControlCatalog.Pages; + +public partial class PulseHomeView : UserControl +{ + public Action? WorkoutDetailRequested { get; set; } + + public PulseHomeView() => InitializeComponent(); + + void OnRecCard1Pressed(object? sender, PointerPressedEventArgs e) => + WorkoutDetailRequested?.Invoke(); + + void OnRecCard2Pressed(object? sender, PointerPressedEventArgs e) => + WorkoutDetailRequested?.Invoke(); + + void OnRecCard3Pressed(object? sender, PointerPressedEventArgs e) => + WorkoutDetailRequested?.Invoke(); + + void OnPlayButtonClicked(object? sender, RoutedEventArgs e) => + WorkoutDetailRequested?.Invoke(); +} diff --git a/samples/ControlCatalog/Pages/NavigationPage/PulseLoginView.xaml b/samples/ControlCatalog/Pages/NavigationPage/PulseLoginView.xaml new file mode 100644 index 0000000000..2309090409 --- /dev/null +++ b/samples/ControlCatalog/Pages/NavigationPage/PulseLoginView.xaml @@ -0,0 +1,145 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/samples/ControlCatalog/Pages/NavigationPage/PulseWorkoutDetailView.xaml.cs b/samples/ControlCatalog/Pages/NavigationPage/PulseWorkoutDetailView.xaml.cs new file mode 100644 index 0000000000..7999d3d709 --- /dev/null +++ b/samples/ControlCatalog/Pages/NavigationPage/PulseWorkoutDetailView.xaml.cs @@ -0,0 +1,15 @@ +using System; +using Avalonia.Controls; +using Avalonia.Interactivity; + +namespace ControlCatalog.Pages; + +public partial class PulseWorkoutDetailView : UserControl +{ + public Action? BackRequested { get; set; } + + public PulseWorkoutDetailView() => InitializeComponent(); + + void OnBackClicked(object? sender, RoutedEventArgs e) => + BackRequested?.Invoke(); +} diff --git a/samples/ControlCatalog/Pages/NavigationPage/PulseWorkoutsView.xaml b/samples/ControlCatalog/Pages/NavigationPage/PulseWorkoutsView.xaml new file mode 100644 index 0000000000..c1f9659b05 --- /dev/null +++ b/samples/ControlCatalog/Pages/NavigationPage/PulseWorkoutsView.xaml @@ -0,0 +1,290 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/samples/ControlCatalog/Pages/NavigationPage/PulseWorkoutsView.xaml.cs b/samples/ControlCatalog/Pages/NavigationPage/PulseWorkoutsView.xaml.cs new file mode 100644 index 0000000000..fec95be3f4 --- /dev/null +++ b/samples/ControlCatalog/Pages/NavigationPage/PulseWorkoutsView.xaml.cs @@ -0,0 +1,8 @@ +using Avalonia.Controls; + +namespace ControlCatalog.Pages; + +public partial class PulseWorkoutsView : UserControl +{ + public PulseWorkoutsView() => InitializeComponent(); +} diff --git a/samples/ControlCatalog/Pages/NavigationPage/RetroGamingAppPage.xaml b/samples/ControlCatalog/Pages/NavigationPage/RetroGamingAppPage.xaml new file mode 100644 index 0000000000..23278161df --- /dev/null +++ b/samples/ControlCatalog/Pages/NavigationPage/RetroGamingAppPage.xaml @@ -0,0 +1,179 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/samples/ControlCatalog/Pages/NavigationPage/RetroGamingAppPage.xaml.cs b/samples/ControlCatalog/Pages/NavigationPage/RetroGamingAppPage.xaml.cs new file mode 100644 index 0000000000..25091493ea --- /dev/null +++ b/samples/ControlCatalog/Pages/NavigationPage/RetroGamingAppPage.xaml.cs @@ -0,0 +1,335 @@ +using System; +using System.Collections.ObjectModel; +using System.Threading.Tasks; +using Avalonia; +using Avalonia.Animation; +using Avalonia.Controls; +using Avalonia.Interactivity; +using Avalonia.Media; + +namespace ControlCatalog.Pages; + +public partial class RetroGamingAppPage : UserControl +{ + static readonly Color BgColor = Color.Parse("#120a1f"); + static readonly Color SurfaceColor = Color.Parse("#2d1b4e"); + static readonly Color CyanColor = Color.Parse("#00ffff"); + static readonly Color YellowColor = Color.Parse("#ffff00"); + static readonly Color MutedColor = Color.Parse("#7856a8"); + static readonly Color TextColor = Color.Parse("#e0d0ff"); + + NavigationPage? _nav; + ScrollViewer? _infoPanel; + + public RetroGamingAppPage() + { + InitializeComponent(); + + _nav = this.FindControl("RetroNav"); + if (_nav != null) + _ = _nav.PushAsync(BuildHomePage()); + } + + protected override void OnLoaded(RoutedEventArgs e) + { + base.OnLoaded(e); + + _infoPanel = this.FindControl("InfoPanel"); + UpdateInfoPanelVisibility(); + } + + protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change) + { + base.OnPropertyChanged(change); + if (change.Property == BoundsProperty) + UpdateInfoPanelVisibility(); + } + + void UpdateInfoPanelVisibility() + { + if (_infoPanel != null) + _infoPanel.IsVisible = Bounds.Width >= 650; + } + + void ApplyHomeNavigationBarAppearance() + { + if (_nav == null) + return; + + _nav.Resources["NavigationBarBackground"] = new SolidColorBrush(SurfaceColor); + _nav.Resources["NavigationBarForeground"] = new SolidColorBrush(CyanColor); + } + + void ApplyDetailNavigationBarAppearance() + { + if (_nav == null) + return; + + _nav.Resources["NavigationBarBackground"] = Brushes.Transparent; + _nav.Resources["NavigationBarForeground"] = new SolidColorBrush(CyanColor); + } + + ContentPage BuildHomePage() + { + var page = new ContentPage { Background = new SolidColorBrush(BgColor) }; + page.Header = BuildPixelArcadeLogo(); + NavigationPage.SetTopCommandBar(page, BuildNavBarRight()); + ApplyHomeNavigationBarAppearance(); + + var panel = new Panel(); + panel.Children.Add(BuildHomeTabbedPage()); + panel.Children.Add(BuildSearchFab()); + + page.Content = panel; + return page; + } + + static Control BuildPixelArcadeLogo() + { + var row = new StackPanel + { + Orientation = Avalonia.Layout.Orientation.Horizontal, + Spacing = 10, + VerticalAlignment = Avalonia.Layout.VerticalAlignment.Center, + }; + + var iconPanel = new Grid { Width = 36, Height = 30 }; + iconPanel.Children.Add(new Border + { + Width = 36, Height = 20, CornerRadius = new CornerRadius(3), + Background = new SolidColorBrush(Color.Parse("#cc44dd")), + VerticalAlignment = Avalonia.Layout.VerticalAlignment.Bottom, + }); + iconPanel.Children.Add(new Border + { + Width = 9, Height = 9, + Background = new SolidColorBrush(SurfaceColor), + HorizontalAlignment = Avalonia.Layout.HorizontalAlignment.Left, + VerticalAlignment = Avalonia.Layout.VerticalAlignment.Bottom, + Margin = new Thickness(4, 0, 0, 6), + }); + iconPanel.Children.Add(new Border + { + Width = 9, Height = 9, + Background = new SolidColorBrush(SurfaceColor), + HorizontalAlignment = Avalonia.Layout.HorizontalAlignment.Right, + VerticalAlignment = Avalonia.Layout.VerticalAlignment.Bottom, + Margin = new Thickness(0, 0, 4, 6), + }); + row.Children.Add(iconPanel); + + var textStack = new StackPanel { Spacing = 1 }; + textStack.Children.Add(new TextBlock + { + Text = "PIXEL", + FontFamily = new FontFamily("Courier New, monospace"), + FontSize = 14, FontWeight = FontWeight.Bold, + Foreground = new SolidColorBrush(YellowColor), LineHeight = 16, + }); + textStack.Children.Add(new TextBlock + { + Text = "ARCADE", + FontFamily = new FontFamily("Courier New, monospace"), + FontSize = 14, FontWeight = FontWeight.Bold, + Foreground = new SolidColorBrush(YellowColor), LineHeight = 16, + }); + row.Children.Add(textStack); + return row; + } + + static Control BuildNavBarRight() + { + var row = new StackPanel + { + Orientation = Avalonia.Layout.Orientation.Horizontal, + Spacing = 10, + VerticalAlignment = Avalonia.Layout.VerticalAlignment.Center, + Margin = new Thickness(0, 0, 8, 0), + }; + row.Children.Add(new PathIcon + { + Width = 16, Height = 16, + Foreground = new SolidColorBrush(TextColor), + Data = Geometry.Parse("M12 22c1.1 0 2-.9 2-2h-4c0 1.1.9 2 2 2zm6-6v-5c0-3.07-1.64-5.64-4.5-6.32V4c0-.83-.67-1.5-1.5-1.5s-1.5.67-1.5 1.5v.68C7.63 5.36 6 7.92 6 11v5l-2 2v1h16v-1l-2-2z"), + }); + var avatar = new Border + { + Width = 26, Height = 26, + CornerRadius = new CornerRadius(0), + ClipToBounds = true, + Background = new SolidColorBrush(SurfaceColor), + BorderBrush = new SolidColorBrush(MutedColor), + BorderThickness = new Thickness(1), + }; + avatar.Child = new TextBlock + { + Text = "P1", + FontFamily = new FontFamily("Courier New, monospace"), + FontSize = 7, FontWeight = FontWeight.Bold, + Foreground = new SolidColorBrush(CyanColor), + HorizontalAlignment = Avalonia.Layout.HorizontalAlignment.Center, + VerticalAlignment = Avalonia.Layout.VerticalAlignment.Center, + }; + row.Children.Add(avatar); + return row; + } + + TabbedPage BuildHomeTabbedPage() + { + var tp = new TabbedPage + { + Background = new SolidColorBrush(BgColor), + TabPlacement = TabPlacement.Bottom, + PageTransition = new PageSlide(TimeSpan.FromMilliseconds(250)), + }; + tp.Resources["TabItemHeaderFontSize"] = 12.0; + tp.Resources["TabbedPageTabStripBackground"] = new SolidColorBrush(SurfaceColor); + tp.Resources["TabbedPageTabItemHeaderForegroundSelected"] = new SolidColorBrush(Color.Parse("#ad2bee")); + tp.Resources["TabbedPageTabItemHeaderForegroundUnselected"] = new SolidColorBrush(MutedColor); + + var homeView = new RetroGamingHomeView(); + homeView.GameSelected = PushDetailPage; + + var homeTab = new ContentPage + { + Header = "Home", + Icon = Geometry.Parse("M10,20V14H14V20H19V12H22L12,3L2,12H5V20H10Z"), + Background = new SolidColorBrush(BgColor), + Content = homeView, + }; + + var gamesView = new RetroGamingGamesView(); + gamesView.GameSelected = PushDetailPage; + + var gamesTab = new ContentPage + { + Header = "Games", + Icon = Geometry.Parse("M7.97,16L5,19C4.67,19.3 4.23,19.5 3.75,19.5A1.75,1.75 0 0,1 2,17.75V17.5L3,10.12C3.21,7.81 5.14,6 7.5,6H16.5C18.86,6 20.79,7.81 21,10.12L22,17.5V17.75A1.75,1.75 0 0,1 20.25,19.5C19.77,19.5 19.33,19.3 19,19L16.03,16H7.97M7,9V11H5V13H7V15H9V13H11V11H9V9H7M14.5,12A1.5,1.5 0 0,0 13,13.5A1.5,1.5 0 0,0 14.5,15A1.5,1.5 0 0,0 16,13.5A1.5,1.5 0 0,0 14.5,12M17.5,9A1.5,1.5 0 0,0 16,10.5A1.5,1.5 0 0,0 17.5,12A1.5,1.5 0 0,0 19,10.5A1.5,1.5 0 0,0 17.5,9Z"), + Background = new SolidColorBrush(BgColor), + Content = gamesView, + }; + + var favTab = new ContentPage + { + Header = "Favorites", + Icon = Geometry.Parse("M12,21.35L10.55,20.03C5.4,15.36 2,12.27 2,8.5C2,5.41 4.42,3 7.5,3C9.24,3 10.91,3.81 12,5.08C13.09,3.81 14.76,3 16.5,3C19.58,3 22,5.41 22,8.5C22,12.27 18.6,15.36 13.45,20.03L12,21.35Z"), + Background = new SolidColorBrush(BgColor), + Content = new RetroGamingFavoritesView(), + }; + + var profileTab = new ContentPage + { + Header = "Profile", + Icon = Geometry.Parse("M12,4A4,4 0 0,1 16,8A4,4 0 0,1 12,12A4,4 0 0,1 8,8A4,4 0 0,1 12,4M12,14C16.42,14 20,15.79 20,18V20H4V18C4,15.79 7.58,14 12,14Z"), + Background = new SolidColorBrush(BgColor), + Content = new RetroGamingProfileView(), + }; + + tp.Pages = new ObservableCollection { homeTab, gamesTab, favTab, profileTab }; + return tp; + } + + Control BuildSearchFab() + { + var fab = new Button + { + Width = 50, Height = 50, + CornerRadius = new CornerRadius(0), + Background = new SolidColorBrush(YellowColor), + Padding = new Thickness(0), + }; + fab.Classes.Add("retro-fab"); + fab.Content = new PathIcon + { + Width = 22, Height = 22, + Foreground = new SolidColorBrush(BgColor), + Data = Geometry.Parse("M9.5,3A6.5,6.5 0 0,1 16,9.5C16,11.11 15.41,12.59 14.44,13.73L14.71,14H15.5L20.5,19L19,20.5L14,15.5V14.71L13.73,14.44C12.59,15.41 11.11,16 9.5,16A6.5,6.5 0 0,1 3,9.5A6.5,6.5 0 0,1 9.5,3M9.5,5C7,5 5,7 5,9.5C5,12 7,14 9.5,14C12,14 14,12 14,9.5C14,7 12,5 9.5,5Z"), + }; + fab.Click += (_, _) => _ = _nav?.PushModalAsync(BuildSearchModal()); + + return new Border + { + HorizontalAlignment = Avalonia.Layout.HorizontalAlignment.Center, + VerticalAlignment = Avalonia.Layout.VerticalAlignment.Bottom, + Margin = new Thickness(0, 0, 0, 35), + BoxShadow = new BoxShadows(new BoxShadow + { + Blur = 10, Spread = 1, + Color = Color.FromArgb(140, 255, 255, 0), + }), + Child = fab, + }; + } + + ContentPage BuildSearchModal() + { + var page = new ContentPage { Background = new SolidColorBrush(BgColor) }; + + var searchView = new RetroGamingSearchView(); + searchView.CloseRequested = () => _ = _nav?.PopModalAsync(); + searchView.GameSelected = async title => + { + await (_nav?.PopModalAsync() ?? System.Threading.Tasks.Task.CompletedTask); + PushDetailPage(title); + }; + + page.Content = searchView; + return page; + } + + async void PushDetailPage(string gameTitle) + { + if (_nav == null) + return; + + var detailView = new RetroGamingDetailView(gameTitle); + + var page = new ContentPage + { + Background = new SolidColorBrush(BgColor), + Content = detailView, + }; + + NavigationPage.SetBarLayoutBehavior(page, BarLayoutBehavior.Overlay); + page.Navigating += args => + { + if (args.NavigationType == NavigationType.Pop) + ApplyHomeNavigationBarAppearance(); + + return Task.CompletedTask; + }; + + var cmdBar = new StackPanel + { + Orientation = Avalonia.Layout.Orientation.Horizontal, + Spacing = 4, + VerticalAlignment = Avalonia.Layout.VerticalAlignment.Center, + Margin = new Thickness(0, 0, 8, 0), + }; + var heartBtn = new Button(); + heartBtn.Classes.Add("retro-icon-btn"); + heartBtn.Content = new PathIcon + { + Width = 16, Height = 16, + Foreground = new SolidColorBrush(Color.Parse("#ad2bee")), + Data = Geometry.Parse("M12,21.35L10.55,20.03C5.4,15.36 2,12.27 2,8.5C2,5.41 4.42,3 7.5,3C9.24,3 10.91,3.81 12,5.08C13.09,3.81 14.76,3 16.5,3C19.58,3 22,5.41 22,8.5C22,12.27 18.6,15.36 13.45,20.03L12,21.35Z"), + }; + var shareBtn = new Button(); + shareBtn.Classes.Add("retro-icon-btn"); + shareBtn.Content = new PathIcon + { + Width = 16, Height = 16, + Foreground = new SolidColorBrush(TextColor), + Data = Geometry.Parse("M18,16.08C17.24,16.08 16.56,16.38 16.04,16.85L8.91,12.7C8.96,12.47 9,12.24 9,12C9,11.76 8.96,11.53 8.91,11.3L15.96,7.19C16.5,7.69 17.21,8 18,8A3,3 0 0,0 21,5A3,3 0 0,0 18,2A3,3 0 0,0 15,5C15,5.24 15.04,5.47 15.09,5.7L8.04,9.81C7.5,9.31 6.79,9 6,9A3,3 0 0,0 3,12A3,3 0 0,0 6,15C6.79,15 7.5,14.69 8.04,14.19L15.16,18.35C15.11,18.56 15.08,18.78 15.08,19C15.08,20.61 16.39,21.92 18,21.92C19.61,21.92 20.92,20.61 20.92,19C20.92,17.39 19.61,16.08 18,16.08Z"), + }; + cmdBar.Children.Add(heartBtn); + cmdBar.Children.Add(shareBtn); + NavigationPage.SetTopCommandBar(page, cmdBar); + + ApplyDetailNavigationBarAppearance(); + await _nav.PushAsync(page); + + if (!ReferenceEquals(_nav.CurrentPage, page)) + ApplyHomeNavigationBarAppearance(); + } +} diff --git a/samples/ControlCatalog/Pages/NavigationPage/RetroGamingDetailView.xaml b/samples/ControlCatalog/Pages/NavigationPage/RetroGamingDetailView.xaml new file mode 100644 index 0000000000..718e8137d2 --- /dev/null +++ b/samples/ControlCatalog/Pages/NavigationPage/RetroGamingDetailView.xaml @@ -0,0 +1,229 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/samples/ControlCatalog/Pages/NavigationPage/RetroGamingDetailView.xaml.cs b/samples/ControlCatalog/Pages/NavigationPage/RetroGamingDetailView.xaml.cs new file mode 100644 index 0000000000..cf49850b84 --- /dev/null +++ b/samples/ControlCatalog/Pages/NavigationPage/RetroGamingDetailView.xaml.cs @@ -0,0 +1,69 @@ +using System; +using System.Collections.Generic; +using Avalonia.Controls; +using Avalonia.Media; +using Avalonia.Media.Imaging; +using Avalonia.Platform; + +namespace ControlCatalog.Pages; + +public partial class RetroGamingDetailView : UserControl +{ + static readonly Dictionary GameAssets = new() + { + { "Cyber Ninja 2084", "hero.jpg" }, + { "Pixel Quest", "pixel_quest.jpg" }, + { "Neon Racer", "neon_racer.jpg" }, + { "Dungeon Bit", "dungeon_bit.jpg" }, + { "Forest Spirit", "forest_spirit.jpg" }, + { "Cyber City", "cyber_city.jpg" }, + { "Neon Ninja", "neon_ninja.jpg" }, + { "Space Voids", "space_voids.jpg" }, + }; + + public RetroGamingDetailView() => InitializeComponent(); + + public RetroGamingDetailView(string gameTitle) + { + InitializeComponent(); + + DetailTitleText.Text = gameTitle.ToUpperInvariant(); + + var filename = GameAssets.TryGetValue(gameTitle, out var f) ? f + : (GameAssets.TryGetValue("Neon Ninja", out var fb) ? fb : null); + + if (filename != null) + { + try + { + var uri = new Uri($"avares://ControlCatalog/Assets/RetroGaming/{filename}"); + using var stream = AssetLoader.Open(uri); + var bmp = new Bitmap(stream); + DetailHeroImageBorder.Background = new ImageBrush(bmp) + { + Stretch = Stretch.UniformToFill, + }; + } + catch + { + SetFallbackBackground(); + } + } + else + { + SetFallbackBackground(); + } + } + + void SetFallbackBackground() + { + var grad = new LinearGradientBrush + { + StartPoint = new Avalonia.RelativePoint(0, 0, Avalonia.RelativeUnit.Relative), + EndPoint = new Avalonia.RelativePoint(1, 1, Avalonia.RelativeUnit.Relative), + }; + grad.GradientStops.Add(new GradientStop(Avalonia.Media.Color.Parse("#3d2060"), 0)); + grad.GradientStops.Add(new GradientStop(Avalonia.Media.Color.Parse("#120a1f"), 1)); + DetailHeroImageBorder.Background = grad; + } +} diff --git a/samples/ControlCatalog/Pages/NavigationPage/RetroGamingFavoritesView.xaml b/samples/ControlCatalog/Pages/NavigationPage/RetroGamingFavoritesView.xaml new file mode 100644 index 0000000000..ef11d4e72d --- /dev/null +++ b/samples/ControlCatalog/Pages/NavigationPage/RetroGamingFavoritesView.xaml @@ -0,0 +1,26 @@ + + + + + + + + + + + + + diff --git a/samples/ControlCatalog/Pages/NavigationPage/RetroGamingFavoritesView.xaml.cs b/samples/ControlCatalog/Pages/NavigationPage/RetroGamingFavoritesView.xaml.cs new file mode 100644 index 0000000000..fd67902a09 --- /dev/null +++ b/samples/ControlCatalog/Pages/NavigationPage/RetroGamingFavoritesView.xaml.cs @@ -0,0 +1,8 @@ +using Avalonia.Controls; + +namespace ControlCatalog.Pages; + +public partial class RetroGamingFavoritesView : UserControl +{ + public RetroGamingFavoritesView() => InitializeComponent(); +} diff --git a/samples/ControlCatalog/Pages/NavigationPage/RetroGamingGamesView.xaml b/samples/ControlCatalog/Pages/NavigationPage/RetroGamingGamesView.xaml new file mode 100644 index 0000000000..a4e9f40387 --- /dev/null +++ b/samples/ControlCatalog/Pages/NavigationPage/RetroGamingGamesView.xaml @@ -0,0 +1,214 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/samples/ControlCatalog/Pages/NavigationPage/RetroGamingGamesView.xaml.cs b/samples/ControlCatalog/Pages/NavigationPage/RetroGamingGamesView.xaml.cs new file mode 100644 index 0000000000..6676b792ab --- /dev/null +++ b/samples/ControlCatalog/Pages/NavigationPage/RetroGamingGamesView.xaml.cs @@ -0,0 +1,39 @@ +using System; +using Avalonia.Controls; +using Avalonia.Layout; + +namespace ControlCatalog.Pages; + +public partial class RetroGamingGamesView : UserControl +{ + public Action? GameSelected { get; set; } + + public RetroGamingGamesView() + { + InitializeComponent(); + + GameCyberNinjaBtn.Click += (_, _) => GameSelected?.Invoke("Cyber Ninja 2084"); + GameNeonRacerBtn.Click += (_, _) => GameSelected?.Invoke("Neon Racer"); + GameDungeonBitBtn.Click += (_, _) => GameSelected?.Invoke("Dungeon Bit"); + GameForestSpiritBtn.Click += (_, _) => GameSelected?.Invoke("Forest Spirit"); + GamePixelQuestBtn.Click += (_, _) => GameSelected?.Invoke("Pixel Quest"); + GameSpaceVoidsBtn.Click += (_, _) => GameSelected?.Invoke("Space Voids"); + GameCyberCityBtn.Click += (_, _) => GameSelected?.Invoke("Cyber City"); + + GamesGrid.SizeChanged += OnGridSizeChanged; + } + + void OnGridSizeChanged(object? sender, SizeChangedEventArgs e) + { + const double defaultWidth = 145; + var available = GamesGrid.Bounds.Width; + if (available <= 0) return; + + bool singleColumn = available < defaultWidth * 2; + foreach (var child in GamesGrid.Children) + { + if (child is Button btn && btn.Content is Border card) + card.Width = singleColumn ? available : defaultWidth; + } + } +} diff --git a/samples/ControlCatalog/Pages/NavigationPage/RetroGamingHomeView.xaml b/samples/ControlCatalog/Pages/NavigationPage/RetroGamingHomeView.xaml new file mode 100644 index 0000000000..bf5d112531 --- /dev/null +++ b/samples/ControlCatalog/Pages/NavigationPage/RetroGamingHomeView.xaml @@ -0,0 +1,346 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/samples/ControlCatalog/Pages/NavigationPage/RetroGamingHomeView.xaml.cs b/samples/ControlCatalog/Pages/NavigationPage/RetroGamingHomeView.xaml.cs new file mode 100644 index 0000000000..d1693f3c6a --- /dev/null +++ b/samples/ControlCatalog/Pages/NavigationPage/RetroGamingHomeView.xaml.cs @@ -0,0 +1,22 @@ +using System; +using Avalonia.Controls; + +namespace ControlCatalog.Pages; + +public partial class RetroGamingHomeView : UserControl +{ + public Action? GameSelected { get; set; } + + public RetroGamingHomeView() + { + InitializeComponent(); + + HeroPlayBtn.Click += (_, _) => GameSelected?.Invoke("Cyber Ninja 2084"); + ContinuePixelQuestBtn.Click += (_, _) => GameSelected?.Invoke("Pixel Quest"); + ContinueSpaceVoidsBtn.Click += (_, _) => GameSelected?.Invoke("Space Voids"); + NewReleaseNeonRacerBtn.Click += (_, _) => GameSelected?.Invoke("Neon Racer"); + NewReleaseDungeonBitBtn.Click += (_, _) => GameSelected?.Invoke("Dungeon Bit"); + NewReleaseForestSpiritBtn.Click += (_, _) => GameSelected?.Invoke("Forest Spirit"); + NewReleaseCyberCityBtn.Click += (_, _) => GameSelected?.Invoke("Cyber City"); + } +} diff --git a/samples/ControlCatalog/Pages/NavigationPage/RetroGamingProfileView.xaml b/samples/ControlCatalog/Pages/NavigationPage/RetroGamingProfileView.xaml new file mode 100644 index 0000000000..17d7ff2d5a --- /dev/null +++ b/samples/ControlCatalog/Pages/NavigationPage/RetroGamingProfileView.xaml @@ -0,0 +1,70 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/samples/ControlCatalog/Pages/NavigationPage/RetroGamingProfileView.xaml.cs b/samples/ControlCatalog/Pages/NavigationPage/RetroGamingProfileView.xaml.cs new file mode 100644 index 0000000000..f2284bdc26 --- /dev/null +++ b/samples/ControlCatalog/Pages/NavigationPage/RetroGamingProfileView.xaml.cs @@ -0,0 +1,8 @@ +using Avalonia.Controls; + +namespace ControlCatalog.Pages; + +public partial class RetroGamingProfileView : UserControl +{ + public RetroGamingProfileView() => InitializeComponent(); +} diff --git a/samples/ControlCatalog/Pages/NavigationPage/RetroGamingSearchView.xaml b/samples/ControlCatalog/Pages/NavigationPage/RetroGamingSearchView.xaml new file mode 100644 index 0000000000..92389efecb --- /dev/null +++ b/samples/ControlCatalog/Pages/NavigationPage/RetroGamingSearchView.xaml @@ -0,0 +1,239 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/samples/ControlCatalog/Pages/NavigationPage/RetroGamingSearchView.xaml.cs b/samples/ControlCatalog/Pages/NavigationPage/RetroGamingSearchView.xaml.cs new file mode 100644 index 0000000000..c5db19c04d --- /dev/null +++ b/samples/ControlCatalog/Pages/NavigationPage/RetroGamingSearchView.xaml.cs @@ -0,0 +1,24 @@ +using System; +using Avalonia.Controls; + +namespace ControlCatalog.Pages; + +public partial class RetroGamingSearchView : UserControl +{ + public Action? CloseRequested { get; set; } + public Action? GameSelected { get; set; } + + public RetroGamingSearchView() + { + InitializeComponent(); + + CloseBtn.Click += (_, _) => CloseRequested?.Invoke(); + SearchCyberNinjaBtn.Click += (_, _) => GameSelected?.Invoke("Cyber Ninja 2084"); + SearchNeonRacerBtn.Click += (_, _) => GameSelected?.Invoke("Neon Racer"); + SearchDungeonBitBtn.Click += (_, _) => GameSelected?.Invoke("Dungeon Bit"); + SearchForestSpiritBtn.Click += (_, _) => GameSelected?.Invoke("Forest Spirit"); + SearchPixelQuestBtn.Click += (_, _) => GameSelected?.Invoke("Pixel Quest"); + SearchSpaceVoidsBtn.Click += (_, _) => GameSelected?.Invoke("Space Voids"); + SearchCyberCityBtn.Click += (_, _) => GameSelected?.Invoke("Cyber City"); + } +} diff --git a/samples/ControlCatalog/Pages/NavigationPage/Transitions/CompositeTransition.cs b/samples/ControlCatalog/Pages/NavigationPage/Transitions/CompositeTransition.cs new file mode 100644 index 0000000000..0cddd58efd --- /dev/null +++ b/samples/ControlCatalog/Pages/NavigationPage/Transitions/CompositeTransition.cs @@ -0,0 +1,120 @@ +using System; +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; +using Avalonia; +using Avalonia.Animation; +using Avalonia.Animation.Easings; +using Avalonia.Media; +using Avalonia.Styling; +using Avalonia.VisualTree; + +namespace ControlCatalog.Pages +{ + /// + /// Example custom IPageTransition: horizontal slide combined with cross-fade. + /// Both pages slide and fade simultaneously for a smooth blended effect. + /// + public class CompositeTransition : IPageTransition + { + public CompositeTransition() { } + + public CompositeTransition(TimeSpan duration) + { + Duration = duration; + } + + public TimeSpan Duration { get; set; } = TimeSpan.FromMilliseconds(300); + public Easing TransitionEasing { get; set; } = new LinearEasing(); + + public async Task Start(Visual? from, Visual? to, bool forward, CancellationToken cancellationToken) + { + if (cancellationToken.IsCancellationRequested) + return; + + var tasks = new List(); + var parent = GetVisualParent(from, to); + var distance = parent.Bounds.Width > 0 ? parent.Bounds.Width : 500d; + + if (from != null) + { + var anim = new Avalonia.Animation.Animation + { + FillMode = FillMode.Forward, + Easing = TransitionEasing, + Duration = Duration, + Children = + { + new KeyFrame + { + Cue = new Cue(0d), + Setters = + { + new Setter(TranslateTransform.XProperty, 0d), + new Setter(Visual.OpacityProperty, 1d) + } + }, + new KeyFrame + { + Cue = new Cue(1d), + Setters = + { + new Setter(TranslateTransform.XProperty, forward ? -distance : distance), + new Setter(Visual.OpacityProperty, 0d) + } + } + } + }; + tasks.Add(anim.RunAsync(from, cancellationToken)); + } + + if (to != null) + { + to.IsVisible = true; + + var anim = new Avalonia.Animation.Animation + { + FillMode = FillMode.Forward, + Easing = TransitionEasing, + Duration = Duration, + Children = + { + new KeyFrame + { + Cue = new Cue(0d), + Setters = + { + new Setter(TranslateTransform.XProperty, forward ? distance : -distance), + new Setter(Visual.OpacityProperty, 0d) + } + }, + new KeyFrame + { + Cue = new Cue(1d), + Setters = + { + new Setter(TranslateTransform.XProperty, 0d), + new Setter(Visual.OpacityProperty, 1d) + } + } + } + }; + tasks.Add(anim.RunAsync(to, cancellationToken)); + } + + await Task.WhenAll(tasks); + + if (from != null && !cancellationToken.IsCancellationRequested) + from.IsVisible = false; + } + + private static Visual GetVisualParent(Visual? from, Visual? to) + { + var p1 = (from ?? to)!.GetVisualParent(); + if (from != null && to != null && + !ReferenceEquals(from.GetVisualParent(), to.GetVisualParent())) + throw new ArgumentException("Transition elements have different parents."); + return p1 ?? throw new ArgumentException("Transition elements have no parent."); + } + } +} diff --git a/samples/ControlCatalog/Pages/NavigationPage/Transitions/FadeThroughTransition.cs b/samples/ControlCatalog/Pages/NavigationPage/Transitions/FadeThroughTransition.cs new file mode 100644 index 0000000000..ece9b1a1de --- /dev/null +++ b/samples/ControlCatalog/Pages/NavigationPage/Transitions/FadeThroughTransition.cs @@ -0,0 +1,137 @@ +using System; +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; +using Avalonia; +using Avalonia.Animation; +using Avalonia.Animation.Easings; +using Avalonia.Media; +using Avalonia.Styling; + +namespace ControlCatalog.Pages +{ + /// + /// Example custom IPageTransition: a "fade through" with scale. + /// The outgoing page fades out while scaling down; the incoming page fades in while + /// scaling up, producing a smooth depth-aware transition. + /// + public class FadeThroughTransition : IPageTransition + { + public FadeThroughTransition() { } + + public FadeThroughTransition(TimeSpan duration) + { + Duration = duration; + } + + public TimeSpan Duration { get; set; } = TimeSpan.FromMilliseconds(300); + public Easing FadeEasing { get; set; } = new CubicEaseOut(); + + public async Task Start(Visual? from, Visual? to, bool forward, CancellationToken cancellationToken) + { + if (cancellationToken.IsCancellationRequested) + return; + + var tasks = new List(); + + if (from != null) + { + from.RenderTransformOrigin = RelativePoint.Center; + from.RenderTransform = new ScaleTransform(1, 1); + } + + if (to != null) + { + to.RenderTransformOrigin = RelativePoint.Center; + to.RenderTransform = new ScaleTransform(1, 1); + to.Opacity = 0; + } + + if (from != null) + { + var outAnim = new Avalonia.Animation.Animation + { + FillMode = FillMode.Forward, + Easing = FadeEasing, + Duration = Duration, + Children = + { + new KeyFrame + { + Cue = new Cue(0d), + Setters = + { + new Setter(Visual.OpacityProperty, 1d), + new Setter(ScaleTransform.ScaleXProperty, 1d), + new Setter(ScaleTransform.ScaleYProperty, 1d) + } + }, + new KeyFrame + { + Cue = new Cue(1d), + Setters = + { + new Setter(Visual.OpacityProperty, 0d), + new Setter(ScaleTransform.ScaleXProperty, forward ? 0.92 : 1.08), + new Setter(ScaleTransform.ScaleYProperty, forward ? 0.92 : 1.08) + } + } + } + }; + tasks.Add(outAnim.RunAsync(from, cancellationToken)); + } + + if (to != null) + { + to.IsVisible = true; + + var inAnim = new Avalonia.Animation.Animation + { + FillMode = FillMode.Forward, + Easing = FadeEasing, + Duration = Duration, + Children = + { + new KeyFrame + { + Cue = new Cue(0d), + Setters = + { + new Setter(Visual.OpacityProperty, 0d), + new Setter(ScaleTransform.ScaleXProperty, forward ? 1.08 : 0.92), + new Setter(ScaleTransform.ScaleYProperty, forward ? 1.08 : 0.92) + } + }, + new KeyFrame + { + Cue = new Cue(1d), + Setters = + { + new Setter(Visual.OpacityProperty, 1d), + new Setter(ScaleTransform.ScaleXProperty, 1d), + new Setter(ScaleTransform.ScaleYProperty, 1d) + } + } + } + }; + tasks.Add(inAnim.RunAsync(to, cancellationToken)); + } + + await Task.WhenAll(tasks); + + if (to != null && !cancellationToken.IsCancellationRequested) + { + to.Opacity = 1; + to.RenderTransform = null; + } + + if (from != null) + { + if (!cancellationToken.IsCancellationRequested) + from.IsVisible = false; + from.Opacity = 1; + from.RenderTransform = null; + } + } + } +} diff --git a/samples/ControlCatalog/Pages/NavigationPage/Transitions/PageSlideTransition.cs b/samples/ControlCatalog/Pages/NavigationPage/Transitions/PageSlideTransition.cs new file mode 100644 index 0000000000..dfbe8478f7 --- /dev/null +++ b/samples/ControlCatalog/Pages/NavigationPage/Transitions/PageSlideTransition.cs @@ -0,0 +1,97 @@ +using System; +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; +using Avalonia; +using Avalonia.Animation; +using Avalonia.Animation.Easings; +using Avalonia.Media; +using Avalonia.Styling; +using Avalonia.VisualTree; + +namespace ControlCatalog.Pages +{ + /// + /// Example custom IPageTransition: a directional page slide. + /// Both pages slide together in the specified axis direction. + /// Demonstrates how to implement a custom horizontal or vertical slide from scratch. + /// + public class PageSlideTransition : IPageTransition + { + public enum SlideAxis { Horizontal, Vertical } + + public PageSlideTransition() { } + + public PageSlideTransition(TimeSpan duration, SlideAxis axis = SlideAxis.Horizontal) + { + Duration = duration; + Axis = axis; + } + + public TimeSpan Duration { get; set; } = TimeSpan.FromMilliseconds(300); + public SlideAxis Axis { get; set; } = SlideAxis.Horizontal; + public Easing SlideEasing { get; set; } = new LinearEasing(); + + public Task Start(Visual? from, Visual? to, bool forward, CancellationToken cancellationToken) => + Axis == SlideAxis.Horizontal + ? StartAxis(from, to, forward, cancellationToken, TranslateTransform.XProperty, () => GetVisualParent(from, to).Bounds.Width) + : StartAxis(from, to, forward, cancellationToken, TranslateTransform.YProperty, () => GetVisualParent(from, to).Bounds.Height); + + private async Task StartAxis( + Visual? from, Visual? to, bool forward, CancellationToken cancellationToken, + Avalonia.AvaloniaProperty prop, Func getDistance) + { + if (cancellationToken.IsCancellationRequested) + return; + var tasks = new List(); + var distance = getDistance() is > 0 and var d ? d : 500d; + + if (from != null) + { + var anim = new Avalonia.Animation.Animation + { + FillMode = FillMode.Forward, + Easing = SlideEasing, + Duration = Duration, + Children = + { + new KeyFrame { Cue = new Cue(0d), Setters = { new Setter(prop, 0d) } }, + new KeyFrame { Cue = new Cue(1d), Setters = { new Setter(prop, forward ? -distance : distance) } } + } + }; + tasks.Add(anim.RunAsync(from, cancellationToken)); + } + + if (to != null) + { + to.IsVisible = true; + var anim = new Avalonia.Animation.Animation + { + FillMode = FillMode.Forward, + Easing = SlideEasing, + Duration = Duration, + Children = + { + new KeyFrame { Cue = new Cue(0d), Setters = { new Setter(prop, forward ? distance : -distance) } }, + new KeyFrame { Cue = new Cue(1d), Setters = { new Setter(prop, 0d) } } + } + }; + tasks.Add(anim.RunAsync(to, cancellationToken)); + } + + await Task.WhenAll(tasks); + + if (from != null && !cancellationToken.IsCancellationRequested) + from.IsVisible = false; + } + + private static Visual GetVisualParent(Visual? from, Visual? to) + { + var p1 = (from ?? to)!.GetVisualParent(); + if (from != null && to != null && + !ReferenceEquals(from.GetVisualParent(), to.GetVisualParent())) + throw new ArgumentException("Transition elements have different parents."); + return p1 ?? throw new ArgumentException("Transition elements have no parent."); + } + } +} diff --git a/samples/ControlCatalog/Pages/NavigationPage/Transitions/ParallaxSlideTransition.cs b/samples/ControlCatalog/Pages/NavigationPage/Transitions/ParallaxSlideTransition.cs new file mode 100644 index 0000000000..7326c6e933 --- /dev/null +++ b/samples/ControlCatalog/Pages/NavigationPage/Transitions/ParallaxSlideTransition.cs @@ -0,0 +1,121 @@ +using System; +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; +using Avalonia; +using Avalonia.Animation; +using Avalonia.Animation.Easings; +using Avalonia.Media; +using Avalonia.Styling; +using Avalonia.VisualTree; + +namespace ControlCatalog.Pages +{ + /// + /// Example custom IPageTransition: a parallax slide. + /// The incoming page slides full-width from the right while the outgoing page shifts ~30% + /// to the left with a subtle opacity fade, producing a depth-layered push effect. + /// + public class ParallaxSlideTransition : IPageTransition + { + public ParallaxSlideTransition() { } + + public ParallaxSlideTransition(TimeSpan duration) + { + Duration = duration; + } + + public TimeSpan Duration { get; set; } = TimeSpan.FromMilliseconds(350); + public Easing SlideEasing { get; set; } = new CubicEaseOut(); + + public async Task Start(Visual? from, Visual? to, bool forward, CancellationToken cancellationToken) + { + if (cancellationToken.IsCancellationRequested) + return; + + var tasks = new List(); + var parent = GetVisualParent(from, to); + var distance = parent.Bounds.Width > 0 ? parent.Bounds.Width : 500d; + + if (from != null) + { + var anim = new Avalonia.Animation.Animation + { + FillMode = FillMode.Forward, + Easing = SlideEasing, + Duration = Duration, + Children = + { + new KeyFrame + { + Cue = new Cue(0d), + Setters = + { + new Setter(TranslateTransform.XProperty, 0d), + new Setter(Visual.OpacityProperty, 1d) + } + }, + new KeyFrame + { + Cue = new Cue(1d), + Setters = + { + new Setter(TranslateTransform.XProperty, forward ? -distance * 0.3 : distance), + new Setter(Visual.OpacityProperty, forward ? 0.7 : 1d) + } + } + } + }; + tasks.Add(anim.RunAsync(from, cancellationToken)); + } + + if (to != null) + { + to.IsVisible = true; + + var anim = new Avalonia.Animation.Animation + { + FillMode = FillMode.Forward, + Easing = SlideEasing, + Duration = Duration, + Children = + { + new KeyFrame + { + Cue = new Cue(0d), + Setters = + { + new Setter(TranslateTransform.XProperty, forward ? distance : -distance * 0.3), + new Setter(Visual.OpacityProperty, forward ? 1d : 0.7) + } + }, + new KeyFrame + { + Cue = new Cue(1d), + Setters = + { + new Setter(TranslateTransform.XProperty, 0d), + new Setter(Visual.OpacityProperty, 1d) + } + } + } + }; + tasks.Add(anim.RunAsync(to, cancellationToken)); + } + + await Task.WhenAll(tasks); + + if (from != null && !cancellationToken.IsCancellationRequested) + from.IsVisible = false; + } + + private static Visual GetVisualParent(Visual? from, Visual? to) + { + var p1 = (from ?? to)!.GetVisualParent(); + if (from != null && to != null && + !ReferenceEquals(from.GetVisualParent(), to.GetVisualParent())) + throw new ArgumentException("Transition elements have different parents."); + return p1 ?? throw new ArgumentException("Transition elements have no parent."); + } + } +} diff --git a/samples/ControlCatalog/Pages/NavigationPerformanceMonitorHelper.cs b/samples/ControlCatalog/Pages/NavigationPerformanceMonitorHelper.cs new file mode 100644 index 0000000000..3178c35fae --- /dev/null +++ b/samples/ControlCatalog/Pages/NavigationPerformanceMonitorHelper.cs @@ -0,0 +1,275 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using Avalonia; +using Avalonia.Controls; +using Avalonia.Layout; +using Avalonia.Media; +using Avalonia.Threading; + +namespace ControlCatalog.Pages +{ + /// + /// Shared helpers for the performance-monitor demo pages + /// (NavigationPage, TabbedPage, DrawerPage, ContentPage). + /// + internal sealed class NavigationPerformanceMonitorHelper + { + internal static readonly IBrush PositiveDeltaBrush = new SolidColorBrush(Color.Parse("#D32F2F")); + internal static readonly IBrush NegativeDeltaBrush = new SolidColorBrush(Color.Parse("#388E3C")); + internal static readonly IBrush ZeroDeltaBrush = new SolidColorBrush(Color.Parse("#757575")); + internal static readonly IBrush CurrentBorderBrush = new SolidColorBrush(Color.Parse("#0078D4")); + internal static readonly IBrush DefaultBorderBrush = new SolidColorBrush(Color.Parse("#CCCCCC")); + + private readonly List> _trackedPages = new(); + private double _previousHeapMB; + private DispatcherTimer? _autoRefreshTimer; + + internal readonly Stopwatch OpStopwatch = new(); + internal int TotalCreated; + + /// + /// Track a newly-created page via WeakReference and increment TotalCreated. + /// + internal void TrackPage(Page page) + { + TotalCreated++; + _trackedPages.Add(new WeakReference(page)); + } + + /// + /// Count live (not yet GC'd) tracked page instances. + /// + internal int CountLiveInstances() + { + int alive = 0; + for (int i = _trackedPages.Count - 1; i >= 0; i--) + { + if (_trackedPages[i].TryGetTarget(out _)) + alive++; + else + _trackedPages.RemoveAt(i); + } + return alive; + } + + /// + /// Update heap and delta text blocks. Call from RefreshAll(). + /// + internal void UpdateHeapDelta(TextBlock heapText, TextBlock deltaText) + { + var heapMB = GC.GetTotalMemory(false) / (1024.0 * 1024.0); + heapText.Text = $"Managed Heap: {heapMB:##0.0} MB"; + + var delta = heapMB - _previousHeapMB; + if (Math.Abs(delta) < 0.05) + { + deltaText.Text = "(no change)"; + deltaText.Foreground = ZeroDeltaBrush; + } + else + { + var sign = delta > 0 ? "+" : ""; + deltaText.Text = $"({sign}{delta:0.0} MB)"; + deltaText.Foreground = delta > 0 ? PositiveDeltaBrush : NegativeDeltaBrush; + } + _previousHeapMB = heapMB; + } + + /// + /// Initialize previous heap baseline. + /// + internal void InitHeap() + { + _previousHeapMB = GC.GetTotalMemory(false) / (1024.0 * 1024.0); + } + + /// + /// Stop the stopwatch and write elapsed ms to the given TextBlock. + /// + internal void StopMetrics(TextBlock lastOpText) + { + if (!OpStopwatch.IsRunning) return; + OpStopwatch.Stop(); + lastOpText.Text = $"Last Op: {OpStopwatch.ElapsedMilliseconds} ms"; + } + + /// + /// Force full GC, then invoke the refresh callback. + /// + internal void ForceGC(Action refresh) + { + GC.Collect(); + GC.WaitForPendingFinalizers(); + GC.Collect(); + refresh(); + } + + /// + /// Start a 2-second auto-refresh timer. + /// + internal void StartAutoRefresh(Action refresh) + { + if (_autoRefreshTimer != null) return; + _autoRefreshTimer = new DispatcherTimer { Interval = TimeSpan.FromSeconds(2) }; + _autoRefreshTimer.Tick += (_, _) => refresh(); + _autoRefreshTimer.Start(); + } + + /// + /// Stop the auto-refresh timer. + /// + internal void StopAutoRefresh() + { + _autoRefreshTimer?.Stop(); + _autoRefreshTimer = null; + } + + /// + /// Toggle auto-refresh based on a CheckBox. + /// + internal void OnAutoRefreshChanged(CheckBox check, Action refresh) + { + if (check.IsChecked == true) + StartAutoRefresh(refresh); + else + StopAutoRefresh(); + } + + /// + /// Append a timestamped log entry to a StackPanel inside a ScrollViewer. + /// + internal void LogOperation(string action, string detail, + StackPanel logPanel, ScrollViewer logScroll, string? extraInfo = null) + { + var heapMB = GC.GetTotalMemory(false) / (1024.0 * 1024.0); + var timing = OpStopwatch.ElapsedMilliseconds; + var extra = extraInfo != null ? $" {extraInfo}," : ""; + + logPanel.Children.Add(new TextBlock + { + Text = $"{DateTime.Now:HH:mm:ss} [{action}] {detail} —{extra} heap {heapMB:##0.0} MB, {timing} ms", + FontSize = 10, + FontFamily = new FontFamily("Cascadia Mono,Consolas,Menlo,monospace"), + Padding = new Thickness(6, 2), + TextTrimming = TextTrimming.CharacterEllipsis, + }); + logScroll.ScrollToEnd(); + } + + /// + /// Build a tracked ContentPage with a 50 KB dummy allocation. + /// + internal ContentPage BuildTrackedPage(string title, int index, int allocBytes = 51200) + { + var page = NavigationDemoHelper.MakePage(title, + $"Stack position #{index}\nPush more pages ...", index); + page.Tag = new byte[allocBytes]; + TrackPage(page); + return page; + } + + /// + /// Create a reusable stack/history row (badge + title + label). + /// + internal static (Border Container, Border Badge, TextBlock IndexText, + TextBlock TitleText, TextBlock BadgeText) CreateStackRow() + { + var indexText = new TextBlock + { + FontSize = 10, FontWeight = FontWeight.SemiBold, + HorizontalAlignment = HorizontalAlignment.Center, + VerticalAlignment = VerticalAlignment.Center, + }; + var badge = new Border + { + Width = 22, Height = 22, + CornerRadius = new CornerRadius(11), + VerticalAlignment = VerticalAlignment.Center, + Child = indexText, + }; + var titleText = new TextBlock + { + VerticalAlignment = VerticalAlignment.Center, + TextTrimming = TextTrimming.CharacterEllipsis, + Margin = new Thickness(6, 0, 0, 0), + }; + var badgeText = new TextBlock + { + FontSize = 10, Opacity = 0.5, + VerticalAlignment = VerticalAlignment.Center, + Margin = new Thickness(4, 0, 0, 0), + IsVisible = false, + }; + + var row = new DockPanel(); + row.Children.Add(badge); + row.Children.Add(titleText); + row.Children.Add(badgeText); + + var container = new Border + { + CornerRadius = new CornerRadius(6), + Padding = new Thickness(8, 6), + Child = row, + }; + + return (container, badge, indexText, titleText, badgeText); + } + + /// + /// Update a stack row with page data. + /// + internal static void UpdateStackRow( + (Border Container, Border Badge, TextBlock IndexText, + TextBlock TitleText, TextBlock BadgeText) row, + int stackIndex, string title, bool isCurrent, bool isRoot) + { + row.Badge.Background = NavigationDemoHelper.GetPageBrush(stackIndex); + row.IndexText.Text = (stackIndex + 1).ToString(); + row.TitleText.Text = title; + row.TitleText.FontWeight = isCurrent ? FontWeight.SemiBold : FontWeight.Normal; + + string? label = isCurrent ? "current" : (isRoot ? "root" : null); + row.BadgeText.Text = label ?? ""; + row.BadgeText.IsVisible = label != null; + + row.Container.BorderBrush = isCurrent ? CurrentBorderBrush : DefaultBorderBrush; + row.Container.BorderThickness = new Thickness(isCurrent ? 2 : 1); + } + + /// + /// Sync a StackPanel of stack rows with data, growing/shrinking the row cache as needed. + /// + internal static void RefreshStackPanel( + StackPanel panel, + List<(Border Container, Border Badge, TextBlock IndexText, + TextBlock TitleText, TextBlock BadgeText)> rowCache, + IReadOnlyList stack, Page? currentPage) + { + int count = stack.Count; + + while (rowCache.Count < count) + rowCache.Add(CreateStackRow()); + + while (panel.Children.Count > count) + panel.Children.RemoveAt(panel.Children.Count - 1); + while (panel.Children.Count < count) + panel.Children.Add(rowCache[panel.Children.Count].Container); + + for (int displayIdx = 0; displayIdx < count; displayIdx++) + { + int stackIdx = count - 1 - displayIdx; + var page = stack[stackIdx]; + bool isCurrent = ReferenceEquals(page, currentPage); + bool isRoot = stackIdx == 0; + + var row = rowCache[displayIdx]; + if (!ReferenceEquals(panel.Children[displayIdx], row.Container)) + panel.Children[displayIdx] = row.Container; + + UpdateStackRow(row, stackIdx, page.Header?.ToString() ?? "(untitled)", isCurrent, isRoot); + } + } + } +} diff --git a/samples/ControlCatalog/Pages/PipsPager/PipsPagerCarouselPage.xaml b/samples/ControlCatalog/Pages/PipsPager/PipsPagerCarouselPage.xaml new file mode 100644 index 0000000000..b75b5c37c2 --- /dev/null +++ b/samples/ControlCatalog/Pages/PipsPager/PipsPagerCarouselPage.xaml @@ -0,0 +1,50 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/samples/ControlCatalog/Pages/PipsPager/PipsPagerCarouselPage.xaml.cs b/samples/ControlCatalog/Pages/PipsPager/PipsPagerCarouselPage.xaml.cs new file mode 100644 index 0000000000..f42bb10ce9 --- /dev/null +++ b/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(); + } +} diff --git a/samples/ControlCatalog/Pages/PipsPager/PipsPagerCustomButtonsPage.xaml b/samples/ControlCatalog/Pages/PipsPager/PipsPagerCustomButtonsPage.xaml new file mode 100644 index 0000000000..8b9856424d --- /dev/null +++ b/samples/ControlCatalog/Pages/PipsPager/PipsPagerCustomButtonsPage.xaml @@ -0,0 +1,78 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/samples/ControlCatalog/Pages/PipsPager/PipsPagerCustomButtonsPage.xaml.cs b/samples/ControlCatalog/Pages/PipsPager/PipsPagerCustomButtonsPage.xaml.cs new file mode 100644 index 0000000000..4fc74995bc --- /dev/null +++ b/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(); + } +} diff --git a/samples/ControlCatalog/Pages/PipsPager/PipsPagerCustomColorsPage.xaml b/samples/ControlCatalog/Pages/PipsPager/PipsPagerCustomColorsPage.xaml new file mode 100644 index 0000000000..260536d7ae --- /dev/null +++ b/samples/ControlCatalog/Pages/PipsPager/PipsPagerCustomColorsPage.xaml @@ -0,0 +1,59 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/samples/ControlCatalog/Pages/PipsPager/PipsPagerCustomColorsPage.xaml.cs b/samples/ControlCatalog/Pages/PipsPager/PipsPagerCustomColorsPage.xaml.cs new file mode 100644 index 0000000000..a9276f11b0 --- /dev/null +++ b/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(); + } +} diff --git a/samples/ControlCatalog/Pages/PipsPager/PipsPagerCustomTemplatesPage.xaml b/samples/ControlCatalog/Pages/PipsPager/PipsPagerCustomTemplatesPage.xaml new file mode 100644 index 0000000000..fe748b248d --- /dev/null +++ b/samples/ControlCatalog/Pages/PipsPager/PipsPagerCustomTemplatesPage.xaml @@ -0,0 +1,197 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/samples/ControlCatalog/Pages/PipsPager/PipsPagerCustomTemplatesPage.xaml.cs b/samples/ControlCatalog/Pages/PipsPager/PipsPagerCustomTemplatesPage.xaml.cs new file mode 100644 index 0000000000..cce9e6c5e5 --- /dev/null +++ b/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(); + } +} diff --git a/samples/ControlCatalog/Pages/PipsPager/PipsPagerEventsPage.xaml b/samples/ControlCatalog/Pages/PipsPager/PipsPagerEventsPage.xaml new file mode 100644 index 0000000000..a69c101687 --- /dev/null +++ b/samples/ControlCatalog/Pages/PipsPager/PipsPagerEventsPage.xaml @@ -0,0 +1,35 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/samples/ControlCatalog/Pages/PipsPager/PipsPagerEventsPage.xaml.cs b/samples/ControlCatalog/Pages/PipsPager/PipsPagerEventsPage.xaml.cs new file mode 100644 index 0000000000..d97165397a --- /dev/null +++ b/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 _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); + }; + } +} diff --git a/samples/ControlCatalog/Pages/PipsPager/PipsPagerGettingStartedPage.xaml b/samples/ControlCatalog/Pages/PipsPager/PipsPagerGettingStartedPage.xaml new file mode 100644 index 0000000000..5eead2fb31 --- /dev/null +++ b/samples/ControlCatalog/Pages/PipsPager/PipsPagerGettingStartedPage.xaml @@ -0,0 +1,46 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/samples/ControlCatalog/Pages/PipsPager/PipsPagerGettingStartedPage.xaml.cs b/samples/ControlCatalog/Pages/PipsPager/PipsPagerGettingStartedPage.xaml.cs new file mode 100644 index 0000000000..80a1569f30 --- /dev/null +++ b/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(); + } +} diff --git a/samples/ControlCatalog/Pages/PipsPager/PipsPagerLargeCollectionPage.xaml b/samples/ControlCatalog/Pages/PipsPager/PipsPagerLargeCollectionPage.xaml new file mode 100644 index 0000000000..5cc416d413 --- /dev/null +++ b/samples/ControlCatalog/Pages/PipsPager/PipsPagerLargeCollectionPage.xaml @@ -0,0 +1,52 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/samples/ControlCatalog/Pages/PipsPager/PipsPagerLargeCollectionPage.xaml.cs b/samples/ControlCatalog/Pages/PipsPager/PipsPagerLargeCollectionPage.xaml.cs new file mode 100644 index 0000000000..2dc936b544 --- /dev/null +++ b/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(); + } +} diff --git a/samples/ControlCatalog/Pages/PipsPagerPage.xaml b/samples/ControlCatalog/Pages/PipsPagerPage.xaml new file mode 100644 index 0000000000..54112daae0 --- /dev/null +++ b/samples/ControlCatalog/Pages/PipsPagerPage.xaml @@ -0,0 +1,11 @@ + + + + + + + diff --git a/samples/ControlCatalog/Pages/PipsPagerPage.xaml.cs b/samples/ControlCatalog/Pages/PipsPagerPage.xaml.cs new file mode 100644 index 0000000000..33cc6d0fdf --- /dev/null +++ b/samples/ControlCatalog/Pages/PipsPagerPage.xaml.cs @@ -0,0 +1,54 @@ +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 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()), + + ("Showcases", "Care Companion", + "A health care onboarding flow using PipsPager as the page indicator for a CarouselPage.", + () => new CareCompanionAppPage()), + ("Showcases", "Sanctuary", + "A travel discovery app using PipsPager as the page indicator for a CarouselPage.", + () => new SanctuaryShowcasePage()), + }; + + public PipsPagerPage() + { + InitializeComponent(); + Loaded += OnLoaded; + } + + private async void OnLoaded(object? sender, RoutedEventArgs e) + { + await SampleNav.PushAsync(NavigationDemoHelper.CreateGalleryHomePage(SampleNav, Demos), null); + } + } +} diff --git a/samples/ControlCatalog/Pages/TabbedDemoPage.xaml b/samples/ControlCatalog/Pages/TabbedDemoPage.xaml new file mode 100644 index 0000000000..bb1467060b --- /dev/null +++ b/samples/ControlCatalog/Pages/TabbedDemoPage.xaml @@ -0,0 +1,11 @@ + + + + + + + diff --git a/samples/ControlCatalog/Pages/TabbedDemoPage.xaml.cs b/samples/ControlCatalog/Pages/TabbedDemoPage.xaml.cs new file mode 100644 index 0000000000..51da6330a1 --- /dev/null +++ b/samples/ControlCatalog/Pages/TabbedDemoPage.xaml.cs @@ -0,0 +1,96 @@ +using System; +using Avalonia.Controls; +using Avalonia.Interactivity; + +namespace ControlCatalog.Pages +{ + public partial class TabbedDemoPage : UserControl + { + private static readonly (string Group, string Title, string Description, Func Factory)[] Demos = + { + // Overview + ("Overview", "First Look", + "Basic TabbedPage with three tabs, tab placement selector, and selection status.", + () => new TabbedPageFirstLookPage()), + + // Populate + ("Populate", "Page Collection", + "Populate a TabbedPage by adding ContentPage objects directly to the Pages collection.", + () => new TabbedPageCollectionPage()), + ("Populate", "Data Templates", + "Bind TabbedPage to an ObservableCollection, add or remove tabs at runtime, and switch the page template.", + () => new TabbedPageDataTemplatePage()), + + // Appearance + ("Appearance", "Tab Customization", + "Customize tab placement, bar background, selected and unselected tab colors.", + () => new TabbedPageCustomizationPage()), + ("Appearance", "Custom Tab Bar", + "VYNTRA-style custom tab bar with floating pill, brand colours, and system-adaptive theme using only resource overrides and styles.", + () => new TabbedPageCustomTabBarPage()), + ("Appearance", "FAB Tab Bar", + "Social-media-style bottom nav with a central floating action button that triggers a command, not a tab.", + () => new TabbedPageFabPage()), + ("Appearance", "Fluid Nav Bar", + "Inspired by the Flutter fluid_nav_bar vignette. Color themes with animated indicator and icons.", + () => new TabbedPageFluidNavPage()), + + // Features + ("Features", "Programmatic Selection", + "Preset the initial tab with SelectedIndex, jump to any tab programmatically, and respond to SelectionChanged events.", + () => new TabbedPageProgrammaticPage()), + ("Features", "Placement", "Switch the tab bar between Top, Bottom, Left, and Right placements.", + () => new TabbedPagePlacementPage()), + ("Features", "Page Transitions", + "Animate tab switches with CrossFade, PageSlide, or composite transitions.", + () => new TabbedPageTransitionsPage()), + ("Features", "Keyboard Navigation", + "Keyboard shortcuts to navigate between tabs, with a toggle to enable or disable.", + () => new TabbedPageKeyboardPage()), + ("Features", "Swipe Gestures", + "Swipe left/right (Top/Bottom) or up/down (Left/Right) to navigate. Toggle IsGestureEnabled.", + () => new TabbedPageGesturePage()), + ("Features", "Events", + "SelectionChanged, NavigatedTo, and NavigatedFrom events. Switch tabs to see the live event log.", + () => new TabbedPageEventsPage()), + ("Features", "Disabled Tabs", + "IsTabEnabled attached property: disable individual tabs so they cannot be selected.", + () => new TabbedPageDisabledTabsPage()), + + // Performance + ("Performance", "Performance Monitor", + "Track tab count, live page instances, and managed heap size. Observe how GC reclaims memory after removing tabs.", + () => new TabbedPagePerformancePage()), + + // Composition + ("Composition", "With NavigationPage", + "Embed a NavigationPage inside each TabbedPage tab for drill-down navigation.", + () => new TabbedPageWithNavigationPage()), + ("Composition", "With DrawerPage", + "Combine TabbedPage with DrawerPage: a global navigation drawer sits over tabbed content.", + () => new TabbedPageWithDrawerPage()), + + // Showcases + ("Showcases", "Pulse Fitness", + "Fitness app with bottom TabbedPage navigation, NavigationPage drill-down inside tabs, and workout detail screens.", + () => new PulseAppPage()), + ("Showcases", "L'Avenir Restaurant", + "Restaurant app with DrawerPage root, NavigationPage detail, and TabbedPage bottom tabs for Menu, Reservations, and Profile.", + () => new LAvenirAppPage()), + ("Showcases", "Retro Gaming", + "Arcade-style app with NavigationPage header, TabbedPage bottom tabs with CenteredTabPanel, and game detail push.", + () => new RetroGamingAppPage()), + }; + + public TabbedDemoPage() + { + InitializeComponent(); + Loaded += OnLoaded; + } + + private async void OnLoaded(object? sender, RoutedEventArgs e) + { + await SampleNav.PushAsync(NavigationDemoHelper.CreateGalleryHomePage(SampleNav, Demos), null); + } + } +} diff --git a/samples/ControlCatalog/Pages/TabbedPage/CenteredTabPanel.cs b/samples/ControlCatalog/Pages/TabbedPage/CenteredTabPanel.cs new file mode 100644 index 0000000000..6ca5e0ec3e --- /dev/null +++ b/samples/ControlCatalog/Pages/TabbedPage/CenteredTabPanel.cs @@ -0,0 +1,63 @@ +using System; +using Avalonia; +using Avalonia.Controls; + +namespace ControlCatalog.Pages +{ + /// + /// A custom panel that arranges N children in N+1 equally-sized slots, leaving the + /// middle slot empty. Intended for tab bars that need a central action button + /// overlaid on the gap. + /// + /// For N children the split is ⌊N/2⌋ items on the left and ⌈N/2⌉ on the right. + /// Example – 4 tabs: [0][1][ gap ][2][3] (5 equal columns, center is free). + /// + public class CenteredTabPanel : Panel + { + protected override Size MeasureOverride(Size availableSize) + { + int count = Children.Count; + if (count == 0) + return default; + + int slots = count + 1; + bool infiniteWidth = double.IsInfinity(availableSize.Width); + double slotWidth = infiniteWidth ? 60.0 : availableSize.Width / slots; + + double maxHeight = 0; + foreach (var child in Children) + { + child.Measure(new Size(slotWidth, availableSize.Height)); + maxHeight = Math.Max(maxHeight, child.DesiredSize.Height); + } + + // When given finite width, fill it. When infinite (inside a ScrollViewer), + // return a small positive width so the parent allocates real space. + double desiredWidth = infiniteWidth ? slotWidth * slots : availableSize.Width; + if (double.IsNaN(maxHeight) || double.IsInfinity(maxHeight)) + maxHeight = 0; + + return new Size(desiredWidth, maxHeight); + } + + protected override Size ArrangeOverride(Size finalSize) + { + int count = Children.Count; + if (count == 0) + return finalSize; + + int slots = count + 1; + double slotW = finalSize.Width / slots; + int leftCount = count / 2; // items placed to the left of the gap + + for (int i = 0; i < count; i++) + { + // Skip the center slot (leftCount), reserved for the FAB. + int slot = i < leftCount ? i : i + 1; + Children[i].Arrange(new Rect(slot * slotW, 0, slotW, finalSize.Height)); + } + + return finalSize; + } + } +} diff --git a/samples/ControlCatalog/Pages/TabbedPage/FluidNavBar/FluidNavBar.cs b/samples/ControlCatalog/Pages/TabbedPage/FluidNavBar/FluidNavBar.cs new file mode 100644 index 0000000000..a68e57c9a4 --- /dev/null +++ b/samples/ControlCatalog/Pages/TabbedPage/FluidNavBar/FluidNavBar.cs @@ -0,0 +1,620 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using Avalonia; +using Avalonia.Controls; +using Avalonia.Input; +using Avalonia.Media; +using Avalonia.Platform; +using Avalonia.Rendering.SceneGraph; +using Avalonia.Skia; +using Avalonia.Threading; +using SkiaSharp; + +namespace ControlCatalog.Pages +{ + /// + /// A fluid navigation bar that replicates the Flutter fluid_nav_bar vignette. + /// The bar background has a bezier "dip" that travels to the selected tab. + /// Each icon is drawn progressively using SKPathMeasure for the fill animation. + /// + public class FluidNavBar : Control, Avalonia.Rendering.ICustomHitTest + { + internal const double NominalHeight = 56.0; + internal const double CircleRadius = 25.0; + internal const double ActiveFloat = 16.0; // px the circle rises + internal const double IconDrawScale = 0.9; // icon scale within circle + internal const double ScaleCurveScale = 0.50; + internal const double FloatLinearPIn = 0.28; + internal const double FillLinearPIn = 0.25; + internal const double XAnimDuration = 0.620; // s — X bump travel + internal const double YDipDuration = 0.300; // s — dip down + internal const double YBounceDelay = 0.500; // s — wait before bounce + internal const double YBounceDuration = 1.200; // s — elastic bounce up + internal const double FloatUpDuration = 1.666; // s — circle rising + internal const double FloatDownDuration = 0.833; // s — circle falling + + public static readonly StyledProperty> ItemsProperty = + AvaloniaProperty.Register>( + nameof(Items), new List()); + + public static readonly StyledProperty SelectedIndexProperty = + AvaloniaProperty.Register(nameof(SelectedIndex), 0); + + public static readonly StyledProperty BarColorProperty = + AvaloniaProperty.Register(nameof(BarColor), Colors.White); + + public static readonly StyledProperty ButtonColorProperty = + AvaloniaProperty.Register(nameof(ButtonColor), Colors.White); + + public static readonly StyledProperty ActiveIconColorProperty = + AvaloniaProperty.Register(nameof(ActiveIconColor), Colors.Black); + + public static readonly StyledProperty InactiveIconColorProperty = + AvaloniaProperty.Register( + nameof(InactiveIconColor), Color.FromArgb(140, 120, 120, 120)); + + private double _xCurrent = -1; // -1 = not yet initialised + private double _lastWidth = -1; // tracks width changes for resize correction + private double _xStart, _xTarget, _xAnimStartSec; + private double _yValue = 1.0; // 0 = deepest dip, 1 = flat + private double _yDipStartSec; + private bool _yBounceStarted; + private double _yBounceStartSec; + + // per-item (length = Items.Count after OnItemsChanged) + private double[] _floatProgress = Array.Empty(); + private double[] _floatStartSec = Array.Empty(); + private bool[] _floatGoingUp = Array.Empty(); + + // Parsed Skia paths — owned here, disposed on detach / items change + private SKPath?[] _parsedPaths = Array.Empty(); + + private DispatcherTimer? _animTimer; + private readonly Stopwatch _clock = Stopwatch.StartNew(); + private bool _animating; + + public IList Items + { + get => GetValue(ItemsProperty); + set => SetValue(ItemsProperty, value); + } + + public int SelectedIndex + { + get => GetValue(SelectedIndexProperty); + set => SetValue(SelectedIndexProperty, value); + } + + public Color BarColor + { + get => GetValue(BarColorProperty); + set => SetValue(BarColorProperty, value); + } + + public Color ButtonColor + { + get => GetValue(ButtonColorProperty); + set => SetValue(ButtonColorProperty, value); + } + + public Color ActiveIconColor + { + get => GetValue(ActiveIconColorProperty); + set => SetValue(ActiveIconColorProperty, value); + } + + public Color InactiveIconColor + { + get => GetValue(InactiveIconColorProperty); + set => SetValue(InactiveIconColorProperty, value); + } + + public event EventHandler? SelectionChanged; + + public FluidNavBar() + { + ClipToBounds = false; + Height = NominalHeight; + Cursor = new Cursor(StandardCursorType.Hand); + } + + protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change) + { + base.OnPropertyChanged(change); + + if (change.Property == ItemsProperty) + OnItemsChanged(); + else if (change.Property == SelectedIndexProperty) + OnSelectedIndexChanged(change.GetOldValue(), change.GetNewValue()); + else if (change.Property == BarColorProperty + || change.Property == ButtonColorProperty + || change.Property == ActiveIconColorProperty + || change.Property == InactiveIconColorProperty) + InvalidateVisual(); + } + + public bool HitTest(Point point) + { + return point.X >= 0 && point.X <= Bounds.Width + && point.Y >= -(ActiveFloat + CircleRadius) + && point.Y <= Bounds.Height; + } + + protected override void OnPointerPressed(PointerPressedEventArgs e) + { + base.OnPointerPressed(e); + + var n = Items?.Count ?? 0; + if (n == 0 || Bounds.Width <= 0) return; + + var pos = e.GetPosition(this); + var index = (int)(pos.X / (Bounds.Width / n)); + index = Math.Clamp(index, 0, n - 1); + + if (index != SelectedIndex) + { + SetCurrentValue(SelectedIndexProperty, index); + SelectionChanged?.Invoke(this, index); + } + + e.Handled = true; + } + + protected override Size MeasureOverride(Size availableSize) + { + var w = double.IsPositiveInfinity(availableSize.Width) ? 300 : availableSize.Width; + return new Size(w, NominalHeight); + } + + protected override Size ArrangeOverride(Size finalSize) + { + var w = finalSize.Width; + if (w > 0) + { + if (_xCurrent < 0 || _lastWidth < 0) + { + // First layout — snap everything to the current selection + _xCurrent = IndexToX(SelectedIndex, w); + _xTarget = _xCurrent; + _xStart = _xCurrent; + } + else if (Math.Abs(w - _lastWidth) > 0.5) + { + // Width changed (resize) — scale pixel positions proportionally + // so the bump stays over the correct slot + var ratio = w / _lastWidth; + _xCurrent = _xCurrent * ratio; + _xStart = _xStart * ratio; + _xTarget = IndexToX(SelectedIndex, w); + InvalidateVisual(); + } + _lastWidth = w; + } + return new Size(w > 0 ? w : 300, NominalHeight); + } + + public override void Render(DrawingContext context) + { + var w = Bounds.Width; + var h = Bounds.Height; + + if (w <= 0 || h <= 0 || Items == null || Items.Count == 0) return; + + var n = Items.Count; + + // Initialise _xCurrent if layout didn't run yet + if (_xCurrent < 0) + { + _xCurrent = IndexToX(SelectedIndex, w); + _xTarget = _xCurrent; + } + + // Snapshot per-item animation state for this frame + var slotCenters = new double[n]; + var floatOffsets = new double[n]; + var scaleYValues = new double[n]; + var fillAmounts = new double[n]; + + for (int i = 0; i < n; i++) + slotCenters[i] = IndexToX(i, w); + + for (int i = 0; i < n; i++) + { + var p = i < _floatProgress.Length ? _floatProgress[i] : (i == SelectedIndex ? 1.0 : 0.0); + var goUp = i < _floatGoingUp.Length ? _floatGoingUp[i] : (i == SelectedIndex); + + // Float offset — uses LinearPoint(0.28, 0) to delay start, then elastic/quintic easing + var linearP = LinearPoint(p, FloatLinearPIn, 0.0); + var floatEased = goUp ? ElasticOut(linearP, 0.38) : EaseInQuint(linearP); + floatOffsets[i] = ActiveFloat * floatEased; + + // Scale Y squish via CenteredElastic curves + var centered = goUp ? CenteredElasticOut(p, 0.6) : CenteredElasticIn(p, 0.6); + scaleYValues[i] = 0.75 + centered * ScaleCurveScale; + + // Icon fill — LinearPoint(0.25, 1.0) adds a slight draw delay vs float + fillAmounts[i] = LinearPoint(p, FillLinearPIn, 1.0); + } + + // Clamp scaleY to sane range to avoid SVG-transform oddities + for (int i = 0; i < n; i++) + scaleYValues[i] = Math.Max(0.1, Math.Min(1.5, scaleYValues[i])); + + var op = new FluidNavBarRenderOp( + new Rect(0, -(ActiveFloat + CircleRadius), w, h + ActiveFloat + CircleRadius), + (float)w, (float)h, + (float)_xCurrent, (float)_yValue, + slotCenters, floatOffsets, scaleYValues, fillAmounts, + _parsedPaths, + BarColor, ButtonColor, ActiveIconColor, InactiveIconColor); + + context.Custom(op); + } + + private void OnItemsChanged() + { + foreach (var p in _parsedPaths) p?.Dispose(); + + var n = Items?.Count ?? 0; + _parsedPaths = new SKPath?[n]; + _floatProgress = new double[n]; + _floatStartSec = new double[n]; + _floatGoingUp = new bool[n]; + + for (int i = 0; i < n; i++) + { + var svg = Items![i].SvgPath; + if (!string.IsNullOrEmpty(svg)) + _parsedPaths[i] = SKPath.ParseSvgPathData(svg); + } + + var sel = Math.Clamp(SelectedIndex, 0, Math.Max(0, n - 1)); + for (int i = 0; i < n; i++) + { + _floatProgress[i] = i == sel ? 1.0 : 0.0; + _floatGoingUp[i] = i == sel; + } + + _xCurrent = -1; // force re-init on next arrange/render + InvalidateVisual(); + } + + private void OnSelectedIndexChanged(int oldIndex, int newIndex) + { + var n = _floatProgress.Length; + if (n == 0) return; + + newIndex = Math.Clamp(newIndex, 0, n - 1); + oldIndex = Math.Clamp(oldIndex, 0, n - 1); + if (oldIndex == newIndex) return; + + var now = _clock.Elapsed.TotalSeconds; + + // X: slide bump from old to new position + if (_xCurrent < 0 && Bounds.Width > 0) + _xCurrent = IndexToX(oldIndex, Bounds.Width); + + _xStart = _xCurrent; + _xTarget = Bounds.Width > 0 ? IndexToX(newIndex, Bounds.Width) : _xStart; + _xAnimStartSec = now; + + // Y: dip then elastic bounce + _yValue = 1.0; + _yDipStartSec = now; + _yBounceStarted = false; + + // Per-button float + _floatGoingUp[oldIndex] = false; + _floatStartSec[oldIndex] = now; + _floatGoingUp[newIndex] = true; + _floatStartSec[newIndex] = now; + + StartAnimation(); + } + + private void StartAnimation() + { + if (_animating) return; + _animating = true; + _animTimer = new DispatcherTimer( + TimeSpan.FromSeconds(1.0 / 60.0), + DispatcherPriority.Render, + OnAnimTick); + _animTimer.Start(); + } + + private void StopAnimation() + { + _animTimer?.Stop(); + _animTimer = null; + _animating = false; + } + + private void OnAnimTick(object? sender, EventArgs e) + { + var now = _clock.Elapsed.TotalSeconds; + var anyActive = false; + + var xElapsed = now - _xAnimStartSec; + if (xElapsed < XAnimDuration) + { + _xCurrent = _xStart + (_xTarget - _xStart) * (xElapsed / XAnimDuration); + anyActive = true; + } + else + { + _xCurrent = _xTarget; + } + + var yDipElapsed = now - _yDipStartSec; + if (yDipElapsed < YDipDuration) + { + _yValue = 1.0 - yDipElapsed / YDipDuration; + anyActive = true; + } + else + { + _yValue = 0.0; + + if (!_yBounceStarted && yDipElapsed >= YBounceDelay) + { + _yBounceStarted = true; + _yBounceStartSec = now; + } + + if (_yBounceStarted) + { + var bt = now - _yBounceStartSec; + if (bt < YBounceDuration) + { + _yValue = ElasticOut(bt / YBounceDuration, 0.38); + anyActive = true; + } + else + { + _yValue = 1.0; + } + } + } + + for (int i = 0; i < _floatProgress.Length; i++) + { + var elapsed = now - _floatStartSec[i]; + var duration = _floatGoingUp[i] ? FloatUpDuration : FloatDownDuration; + if (elapsed < duration) + { + var t = elapsed / duration; + _floatProgress[i] = _floatGoingUp[i] ? t : 1.0 - t; + anyActive = true; + } + else + { + _floatProgress[i] = _floatGoingUp[i] ? 1.0 : 0.0; + } + } + + InvalidateVisual(); + + if (!anyActive) + StopAnimation(); + } + + private double IndexToX(int index, double width) + { + var n = Items?.Count ?? 1; + if (n <= 0) n = 1; + return (index + 0.5) * (width / n); + } + + + internal static double ElasticOut(double t, double period = 0.4) + { + if (t <= 0) return 0; + if (t >= 1) return 1; + var s = period / 4.0; + return Math.Pow(2.0, -10.0 * t) * Math.Sin((t - s) * 2.0 * Math.PI / period) + 1.0; + } + + private static double CenteredElasticOut(double t, double period = 0.4) + { + return Math.Pow(2.0, -10.0 * t) * Math.Sin(t * 2.0 * Math.PI / period) + 0.5; + } + + private static double CenteredElasticIn(double t, double period = 0.4) + { + return -Math.Pow(2.0, 10.0 * (t - 1.0)) * Math.Sin((t - 1.0) * 2.0 * Math.PI / period) + 0.5; + } + + internal static double LinearPoint(double x, double pIn, double pOut) + { + if (pIn <= 0) return pOut; + var lowerScale = pOut / pIn; + var upperScale = (1.0 - pOut) / (1.0 - pIn); + var upperOff = 1.0 - upperScale; + return x < pIn ? x * lowerScale : x * upperScale + upperOff; + } + + private static double EaseInQuint(double t) => t * t * t * t * t; + + protected override void OnDetachedFromVisualTree(VisualTreeAttachmentEventArgs e) + { + base.OnDetachedFromVisualTree(e); + StopAnimation(); + } + + private sealed class FluidNavBarRenderOp : ICustomDrawOperation + { + private readonly float _w, _h, _xCenter, _normY; + private readonly double[] _slots, _floatOff, _scaleY, _fill; + private readonly SKPath?[] _paths; + private readonly Color _bar, _btn, _active, _inactive; + + public Rect Bounds { get; } + + public FluidNavBarRenderOp( + Rect bounds, + float w, float h, + float xCenter, float normY, + double[] slots, double[] floatOff, double[] scaleY, double[] fill, + SKPath?[] paths, + Color bar, Color btn, Color active, Color inactive) + { + Bounds = bounds; + _w = w; _h = h; + _xCenter = xCenter; _normY = normY; + _slots = slots; _floatOff = floatOff; + _scaleY = scaleY; _fill = fill; + _paths = paths; + _bar = bar; _btn = btn; + _active = active; _inactive = inactive; + } + + public bool HitTest(Point p) => false; + public bool Equals(ICustomDrawOperation? other) => false; + public void Dispose() { } + + public void Render(ImmediateDrawingContext context) + { + var lease = context.TryGetFeature(); + if (lease == null) return; + + using var l = lease.Lease(); + var canvas = l.SkCanvas; + + int save = canvas.Save(); + try + { + DrawBackground(canvas); + for (int i = 0; i < _slots.Length; i++) + DrawButton(canvas, i); + } + finally + { + canvas.RestoreToCount(save); + } + } + + private void DrawBackground(SKCanvas canvas) + { + const float rTop = 54f, rBot = 44f; + const float hcTop = 0.6f, hcBot = 0.5f; + const float pcTop = 0.35f, pcBot = 0.85f; + const float tY = -10f, bY = 54f; + const float tD = 0f, bD = 6f; + + float norm = (float)(LinearPoint(_normY, 0.5, 2.0) / 2.0); + + float r = Lerp(rTop, rBot, norm); + float anchr = Lerp(r * hcTop, r * hcBot, (float)LinearPoint(norm, 0.5, 0.75)); + float dipc = Lerp(r * pcTop, r * pcBot, (float)LinearPoint(norm, 0.5, 0.80)); + float y = Lerp(tY, bY, (float)LinearPoint(norm, 0.2, 0.70)); + float dist = Lerp(tD, bD, (float)LinearPoint(norm, 0.5, 0.00)); + float x0 = _xCenter - dist / 2f; + float x1 = _xCenter + dist / 2f; + + using var path = new SKPath(); + path.MoveTo(0, 0); + path.LineTo(x0 - r, 0); + path.CubicTo(x0 - r + anchr, 0, x0 - dipc, y, x0, y); + path.LineTo(x1, y); + path.CubicTo(x1 + dipc, y, x1 + r - anchr, 0, x1 + r, 0); + path.LineTo(_w, 0); + path.LineTo(_w, _h); + path.LineTo(0, _h); + path.Close(); + + using var paint = new SKPaint { Color = ToSK(_bar), IsAntialias = true }; + canvas.DrawPath(path, paint); + } + + private void DrawButton(SKCanvas canvas, int i) + { + var cx = (float)_slots[i]; + var cy = _h / 2f; + var fo = (float)_floatOff[i]; + var sy = (float)_scaleY[i]; + var fa = (float)_fill[i]; + + const float r = (float)CircleRadius; + + // Circle — just translated up, not scaled + using var cp = new SKPaint { Color = ToSK(_btn), IsAntialias = true }; + canvas.DrawCircle(cx, cy - fo, r, cp); + + // Icon + if (i < _paths.Length && _paths[i] != null) + DrawIcon(canvas, _paths[i]!, cx, cy - fo, sy, fa); + } + + private void DrawIcon(SKCanvas canvas, SKPath path, float cx, float cy, + float scaleY, float fillAmount) + { + const float s = (float)IconDrawScale; + + int save = canvas.Save(); + canvas.Translate(cx, cy); + canvas.Scale(s, s * scaleY); + + // Grey background stroke (full path, unselected look) + using var bg = new SKPaint + { + Style = SKPaintStyle.Stroke, + StrokeWidth = 2.4f, + StrokeCap = SKStrokeCap.Round, + StrokeJoin = SKStrokeJoin.Round, + Color = ToSK(_inactive), + IsAntialias = true + }; + canvas.DrawPath(path, bg); + + // Foreground stroke, trimmed progressively with SKPathMeasure + if (fillAmount > 0f) + { + using var fg = new SKPaint + { + Style = SKPaintStyle.Stroke, + StrokeWidth = 2.4f, + StrokeCap = SKStrokeCap.Round, + StrokeJoin = SKStrokeJoin.Round, + Color = ToSK(_active), + IsAntialias = true + }; + DrawTrimmedPath(canvas, path, fillAmount, fg); + } + + canvas.RestoreToCount(save); + } + + // Iterates all contours and draws each trimmed to fillAmount of its length. + // Direct port of Flutter's extractPartialPath behavior. + private static void DrawTrimmedPath(SKCanvas canvas, SKPath path, + float fillAmount, SKPaint paint) + { + using var measure = new SKPathMeasure(path, false); + do + { + var len = measure.Length; + if (len <= 0f) continue; + + using var seg = new SKPath(); + if (measure.GetSegment(0f, len * fillAmount, seg, true)) + canvas.DrawPath(seg, paint); + } + while (measure.NextContour()); + } + + private static float Lerp(float a, float b, float t) => a + (b - a) * t; + + private static double LinearPoint(double x, double pIn, double pOut) + { + if (pIn <= 0) return pOut; + var lo = pOut / pIn; + var hi = (1.0 - pOut) / (1.0 - pIn); + return x < pIn ? x * lo : x * hi + (1.0 - hi); + } + + private static SKColor ToSK(Color c) => new SKColor(c.R, c.G, c.B, c.A); + } + } +} diff --git a/samples/ControlCatalog/Pages/TabbedPage/FluidNavBar/FluidNavItem.cs b/samples/ControlCatalog/Pages/TabbedPage/FluidNavBar/FluidNavItem.cs new file mode 100644 index 0000000000..e2c980050e --- /dev/null +++ b/samples/ControlCatalog/Pages/TabbedPage/FluidNavBar/FluidNavItem.cs @@ -0,0 +1,14 @@ +namespace ControlCatalog.Pages +{ + public class FluidNavItem + { + public string SvgPath { get; } + public string Label { get; } + + public FluidNavItem(string svgPath, string label) + { + SvgPath = svgPath; + Label = label; + } + } +} diff --git a/samples/ControlCatalog/Pages/TabbedPage/TabbedPageCollectionPage.xaml b/samples/ControlCatalog/Pages/TabbedPage/TabbedPageCollectionPage.xaml new file mode 100644 index 0000000000..80e63a08a1 --- /dev/null +++ b/samples/ControlCatalog/Pages/TabbedPage/TabbedPageCollectionPage.xaml @@ -0,0 +1,55 @@ + + + + + + + + + + + + + + + + + diff --git a/samples/ControlCatalog/Pages/TabbedPage/TabbedPageFabPage.xaml.cs b/samples/ControlCatalog/Pages/TabbedPage/TabbedPageFabPage.xaml.cs new file mode 100644 index 0000000000..b52bfd4d8a --- /dev/null +++ b/samples/ControlCatalog/Pages/TabbedPage/TabbedPageFabPage.xaml.cs @@ -0,0 +1,45 @@ +using Avalonia.Controls; +using Avalonia.Interactivity; +using Avalonia.Media; + +namespace ControlCatalog.Pages +{ + public partial class TabbedPageFabPage : UserControl + { + private static readonly StreamGeometry FeedGeometry = + StreamGeometry.Parse("M12.9942 2.79444C12.4118 2.30208 11.5882 2.30208 11.0058 2.79444L3.50582 9.39444C3.18607 9.66478 3 10.0634 3 10.4828V20.25C3 20.9404 3.55964 21.5 4.25 21.5H8.25C8.94036 21.5 9.5 20.9404 9.5 20.25V14.75C9.5 14.6119 9.61193 14.5 9.75 14.5H14.25C14.3881 14.5 14.5 14.6119 14.5 14.75V20.25C14.5 20.9404 15.0596 21.5 15.75 21.5H19.75C20.4404 21.5 21 20.9404 21 20.25V10.4828C21 10.0634 20.8139 9.66478 20.4942 9.39444L12.9942 2.79444Z"); + private static readonly StreamGeometry DiscoverGeometry = + StreamGeometry.Parse("M12 2C6.47 2 2 6.47 2 12s4.47 10 10 10 10-4.47 10-10S17.53 2 12 2zm4.24 5.76-3.03 6.55-6.55 3.03L9.69 10.8l6.55-3.04zM12 13.5c-.83 0-1.5-.67-1.5-1.5s.67-1.5 1.5-1.5 1.5.67 1.5 1.5-.67 1.5-1.5 1.5z"); + private static readonly StreamGeometry AlertsGeometry = + StreamGeometry.Parse("M12 22c1.1 0 2-.9 2-2h-4c0 1.1.9 2 2 2zm6-6v-5c0-3.07-1.64-5.64-4.5-6.32V4c0-.83-.67-1.5-1.5-1.5s-1.5.67-1.5 1.5v.68C7.63 5.36 6 7.92 6 11v5l-2 2v1h16v-1l-2-2z"); + private static readonly StreamGeometry ProfileGeometry = + StreamGeometry.Parse("M12 2C9.243 2 7 4.243 7 7s2.243 5 5 5 5-2.243 5-5-2.243-5-5-5zM12 14c-5.523 0-10 3.582-10 8a1 1 0 001 1h18a1 1 0 001-1c0-4.418-4.477-8-10-8z"); + + private int _postCount; + + public TabbedPageFabPage() + { + InitializeComponent(); + SetupIcons(); + + FabButton.Click += OnFabClicked; + TriggerFabButton.Click += OnFabClicked; + } + + private void SetupIcons() + { + FeedPage.Icon = new PathIcon { Data = FeedGeometry }; + DiscoverPage.Icon = new PathIcon { Data = DiscoverGeometry }; + AlertsPage.Icon = new PathIcon { Data = AlertsGeometry }; + ProfilePage.Icon = new PathIcon { Data = ProfileGeometry }; + } + + private void OnFabClicked(object? sender, RoutedEventArgs e) + { + _postCount++; + StatusText.Text = _postCount == 1 + ? "Post created! Check your feed." + : $"{_postCount} posts created!"; + } + } +} diff --git a/samples/ControlCatalog/Pages/TabbedPage/TabbedPageFirstLookPage.xaml b/samples/ControlCatalog/Pages/TabbedPage/TabbedPageFirstLookPage.xaml new file mode 100644 index 0000000000..1dabb6ba0a --- /dev/null +++ b/samples/ControlCatalog/Pages/TabbedPage/TabbedPageFirstLookPage.xaml @@ -0,0 +1,74 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/samples/ControlCatalog/Pages/TabbedPage/TabbedPageWithDrawerPage.xaml.cs b/samples/ControlCatalog/Pages/TabbedPage/TabbedPageWithDrawerPage.xaml.cs new file mode 100644 index 0000000000..88f688d746 --- /dev/null +++ b/samples/ControlCatalog/Pages/TabbedPage/TabbedPageWithDrawerPage.xaml.cs @@ -0,0 +1,114 @@ +using Avalonia.Controls; +using Avalonia.Interactivity; +using Avalonia.Layout; +using Avalonia.Media; + +namespace ControlCatalog.Pages +{ + public partial class TabbedPageWithDrawerPage : UserControl + { + public TabbedPageWithDrawerPage() + { + InitializeComponent(); + Loaded += (_, _) => ShowSection("Home"); + } + + private void OnSectionSelected(object? sender, RoutedEventArgs e) + { + if (sender is Button btn && btn.Tag is string section) + { + ShowSection(section); + DemoDrawer.IsOpen = false; + } + } + + private void ShowSection(string section) + { + SectionHost.Content = section switch + { + "Home" => CreateHomeTabbed(), + _ => CreatePlainPage(section) + }; + } + + private static Control CreatePlainPage(string section) + { + var (subtitle, icon) = section switch + { + "Explore" => ("Discover new content.", "📍"), + "Favorites" => ("Items you've saved.", "❤"), + _ => (string.Empty, string.Empty) + }; + + return new StackPanel + { + HorizontalAlignment = HorizontalAlignment.Center, + VerticalAlignment = VerticalAlignment.Center, + Spacing = 8, + Children = + { + new TextBlock + { + Text = section, + FontSize = 22, + FontWeight = FontWeight.SemiBold, + HorizontalAlignment = HorizontalAlignment.Center + }, + new TextBlock + { + Text = subtitle, + FontSize = 13, + Opacity = 0.7, + TextWrapping = TextWrapping.Wrap, + TextAlignment = TextAlignment.Center, + MaxWidth = 300 + } + } + }; + } + + private static TabbedPage CreateHomeTabbed() => new() + { + TabPlacement = TabPlacement.Bottom, + Pages = new[] + { + new ContentPage + { + Header = "Featured", + Content = new TextBlock + { + Text = "Featured content", + HorizontalAlignment = HorizontalAlignment.Center, + VerticalAlignment = VerticalAlignment.Center, + FontSize = 18, + Opacity = 0.7 + } + }, + new ContentPage + { + Header = "Recent", + Content = new TextBlock + { + Text = "Recent activity", + HorizontalAlignment = HorizontalAlignment.Center, + VerticalAlignment = VerticalAlignment.Center, + FontSize = 18, + Opacity = 0.7 + } + }, + new ContentPage + { + Header = "Popular", + Content = new TextBlock + { + Text = "Popular right now", + HorizontalAlignment = HorizontalAlignment.Center, + VerticalAlignment = VerticalAlignment.Center, + FontSize = 18, + Opacity = 0.7 + } + } + } + }; + } +} diff --git a/samples/ControlCatalog/Pages/TabbedPage/TabbedPageWithNavigationPage.xaml b/samples/ControlCatalog/Pages/TabbedPage/TabbedPageWithNavigationPage.xaml new file mode 100644 index 0000000000..22878ec3c4 --- /dev/null +++ b/samples/ControlCatalog/Pages/TabbedPage/TabbedPageWithNavigationPage.xaml @@ -0,0 +1,50 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/samples/ControlCatalog/Pages/TabbedPage/TabbedPageWithNavigationPage.xaml.cs b/samples/ControlCatalog/Pages/TabbedPage/TabbedPageWithNavigationPage.xaml.cs new file mode 100644 index 0000000000..2b7a5a3a41 --- /dev/null +++ b/samples/ControlCatalog/Pages/TabbedPage/TabbedPageWithNavigationPage.xaml.cs @@ -0,0 +1,100 @@ +using Avalonia.Controls; +using Avalonia.Interactivity; +using Avalonia.Layout; +using Avalonia.Media; + +namespace ControlCatalog.Pages +{ + public partial class TabbedPageWithNavigationPage : UserControl + { + private bool _initialized; + + public TabbedPageWithNavigationPage() + { + InitializeComponent(); + Loaded += OnLoaded; + } + + private async void OnLoaded(object? sender, RoutedEventArgs e) + { + if (_initialized) + return; + + _initialized = true; + await BrowseNav.PushAsync(CreateListPage("Browse", "Items", BrowseNav), null); + await SearchNav.PushAsync(CreateListPage("Search", "Results", SearchNav), null); + await AccountNav.PushAsync(CreateListPage("Account", "Options", AccountNav), null); + } + + private static ContentPage CreateListPage(string tabName, string listTitle, NavigationPage nav) + { + var list = new ListBox + { + Margin = new Avalonia.Thickness(8), + Items = + { + $"{listTitle} item 1", + $"{listTitle} item 2", + $"{listTitle} item 3", + $"{listTitle} item 4", + $"{listTitle} item 5" + } + }; + + list.SelectionChanged += async (_, args) => + { + if (args.AddedItems.Count == 0) return; + var item = args.AddedItems[0]?.ToString() ?? string.Empty; + list.SelectedItem = null; + + var detail = new ContentPage + { + Header = item, + Content = new StackPanel + { + HorizontalAlignment = HorizontalAlignment.Center, + VerticalAlignment = VerticalAlignment.Center, + Spacing = 8, + Children = + { + new TextBlock + { + Text = item, + FontSize = 20, + FontWeight = FontWeight.SemiBold, + HorizontalAlignment = HorizontalAlignment.Center + }, + new TextBlock + { + Text = $"Detail view for \"{item}\" in the {tabName} tab.", + FontSize = 13, + Opacity = 0.7, + TextWrapping = TextWrapping.Wrap, + TextAlignment = Avalonia.Media.TextAlignment.Center, + MaxWidth = 280 + } + } + } + }; + + await nav.PushAsync(detail, nav.PageTransition); + }; + + var page = new ContentPage + { + Header = tabName, + Content = list, + HorizontalContentAlignment = HorizontalAlignment.Stretch, + VerticalContentAlignment = VerticalAlignment.Stretch + }; + NavigationPage.SetHasNavigationBar(page, false); + return page; + } + + private void OnPlacementChanged(object? sender, SelectionChangedEventArgs e) + { + if (DemoTabs == null) return; + DemoTabs.TabPlacement = PlacementCombo.SelectedIndex == 0 ? TabPlacement.Top : TabPlacement.Bottom; + } + } +} diff --git a/samples/ControlCatalog/Pages/Transitions/CardStackPageTransition.cs b/samples/ControlCatalog/Pages/Transitions/CardStackPageTransition.cs new file mode 100644 index 0000000000..89ae1e5e8a --- /dev/null +++ b/samples/ControlCatalog/Pages/Transitions/CardStackPageTransition.cs @@ -0,0 +1,447 @@ +using System; +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; +using Avalonia; +using Avalonia.Animation; +using Avalonia.Animation.Easings; +using Avalonia.Media; +using Avalonia.Styling; + +namespace ControlCatalog.Pages.Transitions; + +/// +/// Transitions between two pages with a card-stack effect: +/// the top page moves/rotates away while the next page scales up underneath. +/// +public class CardStackPageTransition : PageSlide +{ + private const double ViewportLiftScale = 0.03; + private const double ViewportPromotionScale = 0.02; + private const double ViewportDepthOpacityFalloff = 0.08; + private const double SidePeekAngle = 4.0; + private const double FarPeekAngle = 7.0; + + /// + /// Initializes a new instance of the class. + /// + public CardStackPageTransition() + { + } + + /// + /// Initializes a new instance of the class. + /// + /// The duration of the animation. + /// The axis on which the animation should occur. + public CardStackPageTransition(TimeSpan duration, PageSlide.SlideAxis orientation = PageSlide.SlideAxis.Horizontal) + : base(duration, orientation) + { + } + + /// + /// Gets or sets the maximum rotation angle (degrees) applied to the top card. + /// + public double MaxSwipeAngle { get; set; } = 15.0; + + /// + /// Gets or sets the scale reduction applied to the back card (0.05 = 5%). + /// + public double BackCardScale { get; set; } = 0.05; + + /// + /// Gets or sets the vertical offset (pixels) applied to the back card. + /// + public double BackCardOffset { get; set; } = 0.0; + + /// + public override async Task Start(Visual? from, Visual? to, bool forward, CancellationToken cancellationToken) + { + if (cancellationToken.IsCancellationRequested) + { + return; + } + + var tasks = new List(); + var parent = GetVisualParent(from, to); + var distance = Orientation == PageSlide.SlideAxis.Horizontal ? parent.Bounds.Width : parent.Bounds.Height; + var translateProperty = Orientation == PageSlide.SlideAxis.Horizontal ? TranslateTransform.XProperty : TranslateTransform.YProperty; + var rotationTarget = Orientation == PageSlide.SlideAxis.Horizontal ? (forward ? -MaxSwipeAngle : MaxSwipeAngle) : 0.0; + var startScale = 1.0 - BackCardScale; + + if (from != null) + { + var (rotate, translate) = EnsureTopTransforms(from); + rotate.Angle = 0; + translate.X = 0; + translate.Y = 0; + from.Opacity = 1; + from.ZIndex = 1; + + var animation = new Animation + { + Easing = SlideOutEasing, + Duration = Duration, + FillMode = FillMode, + Children = + { + new KeyFrame + { + Setters = + { + new Setter(translateProperty, 0d), + new Setter(RotateTransform.AngleProperty, 0d) + }, + Cue = new Cue(0d) + }, + new KeyFrame + { + Setters = + { + new Setter(translateProperty, forward ? -distance : distance), + new Setter(RotateTransform.AngleProperty, rotationTarget) + }, + Cue = new Cue(1d) + } + } + }; + tasks.Add(animation.RunAsync(from, cancellationToken)); + } + + if (to != null) + { + var (scale, translate) = EnsureBackTransforms(to); + scale.ScaleX = startScale; + scale.ScaleY = startScale; + translate.X = 0; + translate.Y = BackCardOffset; + to.IsVisible = true; + to.Opacity = 1; + to.ZIndex = 0; + + var animation = new Animation + { + Easing = SlideInEasing, + Duration = Duration, + FillMode = FillMode, + Children = + { + new KeyFrame + { + Setters = + { + new Setter(ScaleTransform.ScaleXProperty, startScale), + new Setter(ScaleTransform.ScaleYProperty, startScale), + new Setter(TranslateTransform.YProperty, BackCardOffset) + }, + Cue = new Cue(0d) + }, + new KeyFrame + { + Setters = + { + new Setter(ScaleTransform.ScaleXProperty, 1d), + new Setter(ScaleTransform.ScaleYProperty, 1d), + new Setter(TranslateTransform.YProperty, 0d) + }, + Cue = new Cue(1d) + } + } + }; + + tasks.Add(animation.RunAsync(to, cancellationToken)); + } + + await Task.WhenAll(tasks); + + if (from != null && !cancellationToken.IsCancellationRequested) + { + from.IsVisible = false; + } + + if (!cancellationToken.IsCancellationRequested && to != null) + { + var (scale, translate) = EnsureBackTransforms(to); + scale.ScaleX = 1; + scale.ScaleY = 1; + translate.X = 0; + translate.Y = 0; + } + } + + /// + public override void Update( + double progress, + Visual? from, + Visual? to, + bool forward, + double pageLength, + IReadOnlyList visibleItems) + { + if (visibleItems.Count > 0) + { + UpdateVisibleItems(progress, from, to, forward, pageLength, visibleItems); + return; + } + + if (from is null && to is null) + return; + + var parent = GetVisualParent(from, to); + var size = parent.Bounds.Size; + var isHorizontal = Orientation == PageSlide.SlideAxis.Horizontal; + var distance = pageLength > 0 + ? pageLength + : (isHorizontal ? size.Width : size.Height); + var rotationTarget = isHorizontal ? (forward ? -MaxSwipeAngle : MaxSwipeAngle) : 0.0; + var startScale = 1.0 - BackCardScale; + + if (from != null) + { + var (rotate, translate) = EnsureTopTransforms(from); + if (isHorizontal) + { + translate.X = forward ? -distance * progress : distance * progress; + translate.Y = 0; + } + else + { + translate.X = 0; + translate.Y = forward ? -distance * progress : distance * progress; + } + + rotate.Angle = rotationTarget * progress; + from.IsVisible = true; + from.Opacity = 1; + from.ZIndex = 1; + } + + if (to != null) + { + var (scale, translate) = EnsureBackTransforms(to); + var currentScale = startScale + (1.0 - startScale) * progress; + var currentOffset = BackCardOffset * (1.0 - progress); + + scale.ScaleX = currentScale; + scale.ScaleY = currentScale; + if (isHorizontal) + { + translate.X = 0; + translate.Y = currentOffset; + } + else + { + translate.X = currentOffset; + translate.Y = 0; + } + + to.IsVisible = true; + to.Opacity = 1; + to.ZIndex = 0; + } + } + + /// + public override void Reset(Visual visual) + { + visual.RenderTransform = null; + visual.RenderTransformOrigin = default; + visual.Opacity = 1; + visual.ZIndex = 0; + } + + private void UpdateVisibleItems( + double progress, + Visual? from, + Visual? to, + bool forward, + double pageLength, + IReadOnlyList visibleItems) + { + var isHorizontal = Orientation == PageSlide.SlideAxis.Horizontal; + var rotationTarget = isHorizontal + ? (forward ? -MaxSwipeAngle : MaxSwipeAngle) + : 0.0; + var stackOffset = GetViewportStackOffset(pageLength); + var lift = Math.Sin(Math.Clamp(progress, 0.0, 1.0) * Math.PI); + + foreach (var item in visibleItems) + { + var visual = item.Visual; + var (rotate, scale, translate) = EnsureViewportTransforms(visual); + var depth = GetViewportDepth(item.ViewportCenterOffset); + var scaleValue = Math.Max(0.84, 1.0 - (BackCardScale * depth)); + var stackValue = stackOffset * depth; + var baseOpacity = Math.Max(0.8, 1.0 - (ViewportDepthOpacityFalloff * depth)); + var restingAngle = isHorizontal ? GetViewportRestingAngle(item.ViewportCenterOffset) : 0.0; + + rotate.Angle = restingAngle; + scale.ScaleX = scaleValue; + scale.ScaleY = scaleValue; + translate.X = 0; + translate.Y = 0; + + if (ReferenceEquals(visual, from)) + { + rotate.Angle = restingAngle + (rotationTarget * progress); + stackValue -= stackOffset * 0.2 * lift; + baseOpacity = Math.Min(1.0, baseOpacity + 0.08); + } + + if (ReferenceEquals(visual, to)) + { + var promotedScale = Math.Min(1.0, scaleValue + (ViewportLiftScale * lift) + (ViewportPromotionScale * progress)); + scale.ScaleX = promotedScale; + scale.ScaleY = promotedScale; + rotate.Angle = restingAngle * (1.0 - progress); + stackValue = Math.Max(0.0, stackValue - (stackOffset * (0.45 + (0.2 * lift)) * progress)); + baseOpacity = Math.Min(1.0, baseOpacity + (0.12 * lift)); + } + + if (isHorizontal) + translate.Y = stackValue; + else + translate.X = stackValue; + + visual.IsVisible = true; + visual.Opacity = baseOpacity; + visual.ZIndex = GetViewportZIndex(item.ViewportCenterOffset, visual, from, to); + } + } + + private static (RotateTransform rotate, TranslateTransform translate) EnsureTopTransforms(Visual visual) + { + if (visual.RenderTransform is TransformGroup group && + group.Children.Count == 2 && + group.Children[0] is RotateTransform rotateTransform && + group.Children[1] is TranslateTransform translateTransform) + { + visual.RenderTransformOrigin = new RelativePoint(0.5, 0.5, RelativeUnit.Relative); + return (rotateTransform, translateTransform); + } + + var rotate = new RotateTransform(); + var translate = new TranslateTransform(); + visual.RenderTransform = new TransformGroup + { + Children = + { + rotate, + translate + } + }; + visual.RenderTransformOrigin = new RelativePoint(0.5, 0.5, RelativeUnit.Relative); + return (rotate, translate); + } + + private static (ScaleTransform scale, TranslateTransform translate) EnsureBackTransforms(Visual visual) + { + if (visual.RenderTransform is TransformGroup group && + group.Children.Count == 2 && + group.Children[0] is ScaleTransform scaleTransform && + group.Children[1] is TranslateTransform translateTransform) + { + visual.RenderTransformOrigin = new RelativePoint(0.5, 0.5, RelativeUnit.Relative); + return (scaleTransform, translateTransform); + } + + var scale = new ScaleTransform(); + var translate = new TranslateTransform(); + visual.RenderTransform = new TransformGroup + { + Children = + { + scale, + translate + } + }; + visual.RenderTransformOrigin = new RelativePoint(0.5, 0.5, RelativeUnit.Relative); + return (scale, translate); + } + + private static (RotateTransform rotate, ScaleTransform scale, TranslateTransform translate) EnsureViewportTransforms(Visual visual) + { + if (visual.RenderTransform is TransformGroup group && + group.Children.Count == 3 && + group.Children[0] is RotateTransform rotateTransform && + group.Children[1] is ScaleTransform scaleTransform && + group.Children[2] is TranslateTransform translateTransform) + { + visual.RenderTransformOrigin = new RelativePoint(0.5, 0.5, RelativeUnit.Relative); + return (rotateTransform, scaleTransform, translateTransform); + } + + var rotate = new RotateTransform(); + var scale = new ScaleTransform(1, 1); + var translate = new TranslateTransform(); + visual.RenderTransform = new TransformGroup + { + Children = + { + rotate, + scale, + translate + } + }; + visual.RenderTransformOrigin = new RelativePoint(0.5, 0.5, RelativeUnit.Relative); + return (rotate, scale, translate); + } + + private double GetViewportStackOffset(double pageLength) + { + if (BackCardOffset > 0) + return BackCardOffset; + + return Math.Clamp(pageLength * 0.045, 10.0, 18.0); + } + + private static double GetViewportDepth(double offsetFromCenter) + { + var distance = Math.Abs(offsetFromCenter); + + if (distance <= 1.0) + return distance; + + if (distance <= 2.0) + return 1.0 + ((distance - 1.0) * 0.8); + + return 1.8; + } + + private static double GetViewportRestingAngle(double offsetFromCenter) + { + var sign = Math.Sign(offsetFromCenter); + if (sign == 0) + return 0; + + var distance = Math.Abs(offsetFromCenter); + if (distance <= 1.0) + return sign * Lerp(0.0, SidePeekAngle, distance); + + if (distance <= 2.0) + return sign * Lerp(SidePeekAngle, FarPeekAngle, distance - 1.0); + + return sign * FarPeekAngle; + } + + private static double Lerp(double from, double to, double t) + { + return from + ((to - from) * Math.Clamp(t, 0.0, 1.0)); + } + + private static int GetViewportZIndex(double offsetFromCenter, Visual visual, Visual? from, Visual? to) + { + if (ReferenceEquals(visual, from)) + return 5; + + if (ReferenceEquals(visual, to)) + return 4; + + var distance = Math.Abs(offsetFromCenter); + if (distance < 0.5) + return 4; + if (distance < 1.5) + return 3; + return 2; + } +} diff --git a/samples/ControlCatalog/Pages/Transitions/WaveRevealPageTransition.cs b/samples/ControlCatalog/Pages/Transitions/WaveRevealPageTransition.cs new file mode 100644 index 0000000000..9d8e80bf9c --- /dev/null +++ b/samples/ControlCatalog/Pages/Transitions/WaveRevealPageTransition.cs @@ -0,0 +1,380 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Threading; +using System.Threading.Tasks; +using Avalonia; +using Avalonia.Animation; +using Avalonia.Animation.Easings; +using Avalonia.Media; + +namespace ControlCatalog.Pages.Transitions; + +/// +/// Transitions between two pages using a wave clip that reveals the next page. +/// +public class WaveRevealPageTransition : PageSlide +{ + /// + /// Initializes a new instance of the class. + /// + public WaveRevealPageTransition() + { + } + + /// + /// Initializes a new instance of the class. + /// + /// The duration of the animation. + /// The axis on which the animation should occur. + public WaveRevealPageTransition(TimeSpan duration, PageSlide.SlideAxis orientation = PageSlide.SlideAxis.Horizontal) + : base(duration, orientation) + { + } + + /// + /// Gets or sets the maximum wave bulge (pixels) along the movement axis. + /// + public double MaxBulge { get; set; } = 120.0; + + /// + /// Gets or sets the bulge factor along the movement axis (0-1). + /// + public double BulgeFactor { get; set; } = 0.35; + + /// + /// Gets or sets the bulge factor along the cross axis (0-1). + /// + public double CrossBulgeFactor { get; set; } = 0.3; + + /// + /// Gets or sets a cross-axis offset (pixels) to shift the wave center. + /// + public double WaveCenterOffset { get; set; } = 0.0; + + /// + /// Gets or sets how strongly the wave center follows the provided offset. + /// + public double CenterSensitivity { get; set; } = 1.0; + + /// + /// Gets or sets the bulge exponent used to shape the wave (1.0 = linear). + /// Higher values tighten the bulge; lower values broaden it. + /// + public double BulgeExponent { get; set; } = 1.0; + + /// + /// Gets or sets the easing applied to the wave progress (clip only). + /// + public Easing WaveEasing { get; set; } = new CubicEaseOut(); + + /// + public override async Task Start(Visual? from, Visual? to, bool forward, CancellationToken cancellationToken) + { + if (cancellationToken.IsCancellationRequested) + { + return; + } + + if (to != null) + { + to.IsVisible = true; + to.ZIndex = 1; + } + + if (from != null) + { + from.ZIndex = 0; + } + + await AnimateProgress(0.0, 1.0, from, to, forward, cancellationToken); + + if (to != null && !cancellationToken.IsCancellationRequested) + { + to.Clip = null; + } + + if (from != null && !cancellationToken.IsCancellationRequested) + { + from.IsVisible = false; + } + } + + /// + public override void Update( + double progress, + Visual? from, + Visual? to, + bool forward, + double pageLength, + IReadOnlyList visibleItems) + { + if (visibleItems.Count > 0) + { + UpdateVisibleItems(from, to, forward, pageLength, visibleItems); + return; + } + + if (from is null && to is null) + return; + var parent = GetVisualParent(from, to); + var size = parent.Bounds.Size; + var centerOffset = WaveCenterOffset * CenterSensitivity; + var isHorizontal = Orientation == PageSlide.SlideAxis.Horizontal; + + if (to != null) + { + to.IsVisible = progress > 0.0; + to.ZIndex = 1; + to.Opacity = 1; + + if (progress >= 1.0) + { + to.Clip = null; + } + else + { + var waveProgress = WaveEasing?.Ease(progress) ?? progress; + var clip = LiquidSwipeClipper.CreateWavePath( + waveProgress, + size, + centerOffset, + forward, + isHorizontal, + MaxBulge, + BulgeFactor, + CrossBulgeFactor, + BulgeExponent); + to.Clip = clip; + } + } + + if (from != null) + { + from.IsVisible = true; + from.ZIndex = 0; + from.Opacity = 1; + } + } + + private void UpdateVisibleItems( + Visual? from, + Visual? to, + bool forward, + double pageLength, + IReadOnlyList visibleItems) + { + if (from is null && to is null) + return; + + var parent = GetVisualParent(from, to); + var size = parent.Bounds.Size; + var centerOffset = WaveCenterOffset * CenterSensitivity; + var isHorizontal = Orientation == PageSlide.SlideAxis.Horizontal; + var resolvedPageLength = pageLength > 0 + ? pageLength + : (isHorizontal ? size.Width : size.Height); + foreach (var item in visibleItems) + { + var visual = item.Visual; + visual.IsVisible = true; + visual.Opacity = 1; + visual.Clip = null; + visual.ZIndex = ReferenceEquals(visual, to) ? 1 : 0; + + if (!ReferenceEquals(visual, to)) + continue; + + var visibleFraction = GetVisibleFraction(item.ViewportCenterOffset, size, resolvedPageLength, isHorizontal); + if (visibleFraction >= 1.0) + continue; + + visual.Clip = LiquidSwipeClipper.CreateWavePath( + visibleFraction, + size, + centerOffset, + forward, + isHorizontal, + MaxBulge, + BulgeFactor, + CrossBulgeFactor, + BulgeExponent); + } + } + + private static double GetVisibleFraction(double offsetFromCenter, Size viewportSize, double pageLength, bool isHorizontal) + { + if (pageLength <= 0) + return 1.0; + + var viewportLength = isHorizontal ? viewportSize.Width : viewportSize.Height; + if (viewportLength <= 0) + return 0.0; + + var viewportUnits = viewportLength / pageLength; + var edgePeek = Math.Max(0.0, (viewportUnits - 1.0) / 2.0); + return Math.Clamp(1.0 + edgePeek - Math.Abs(offsetFromCenter), 0.0, 1.0); + } + + /// + public override void Reset(Visual visual) + { + visual.Clip = null; + visual.ZIndex = 0; + visual.Opacity = 1; + } + + private async Task AnimateProgress( + double from, + double to, + Visual? fromVisual, + Visual? toVisual, + bool forward, + CancellationToken cancellationToken) + { + var parent = GetVisualParent(fromVisual, toVisual); + var pageLength = Orientation == PageSlide.SlideAxis.Horizontal + ? parent.Bounds.Width + : parent.Bounds.Height; + var durationMs = Math.Max(Duration.TotalMilliseconds * Math.Abs(to - from), 50); + var startTicks = Stopwatch.GetTimestamp(); + var tickFreq = Stopwatch.Frequency; + + while (!cancellationToken.IsCancellationRequested) + { + var elapsedMs = (Stopwatch.GetTimestamp() - startTicks) * 1000.0 / tickFreq; + var t = Math.Clamp(elapsedMs / durationMs, 0.0, 1.0); + var eased = SlideInEasing?.Ease(t) ?? t; + var progress = from + (to - from) * eased; + + Update(progress, fromVisual, toVisual, forward, pageLength, Array.Empty()); + + if (t >= 1.0) + break; + + await Task.Delay(16, cancellationToken); + } + + if (!cancellationToken.IsCancellationRequested) + { + Update(to, fromVisual, toVisual, forward, pageLength, Array.Empty()); + } + } + + private static class LiquidSwipeClipper + { + public static Geometry CreateWavePath( + double progress, + Size size, + double waveCenterOffset, + bool forward, + bool isHorizontal, + double maxBulge, + double bulgeFactor, + double crossBulgeFactor, + double bulgeExponent) + { + var width = size.Width; + var height = size.Height; + + if (progress <= 0) + return new RectangleGeometry(new Rect(0, 0, 0, 0)); + + if (progress >= 1) + return new RectangleGeometry(new Rect(0, 0, width, height)); + + if (width <= 0 || height <= 0) + return new RectangleGeometry(new Rect(0, 0, 0, 0)); + + var mainLength = isHorizontal ? width : height; + var crossLength = isHorizontal ? height : width; + + var wavePhase = Math.Sin(progress * Math.PI); + var bulgeProgress = bulgeExponent == 1.0 ? wavePhase : Math.Pow(wavePhase, bulgeExponent); + var revealedLength = mainLength * progress; + var bulgeMain = Math.Min(mainLength * bulgeFactor, maxBulge) * bulgeProgress; + bulgeMain = Math.Min(bulgeMain, revealedLength * 0.45); + var bulgeCross = crossLength * crossBulgeFactor; + + var waveCenter = crossLength / 2 + waveCenterOffset; + waveCenter = Math.Clamp(waveCenter, bulgeCross, crossLength - bulgeCross); + + var geometry = new StreamGeometry(); + using (var context = geometry.Open()) + { + if (isHorizontal) + { + if (forward) + { + var waveX = width * (1 - progress); + context.BeginFigure(new Point(width, 0), true); + context.LineTo(new Point(waveX, 0)); + context.CubicBezierTo( + new Point(waveX, waveCenter - bulgeCross), + new Point(waveX - bulgeMain, waveCenter - bulgeCross * 0.5), + new Point(waveX - bulgeMain, waveCenter)); + context.CubicBezierTo( + new Point(waveX - bulgeMain, waveCenter + bulgeCross * 0.5), + new Point(waveX, waveCenter + bulgeCross), + new Point(waveX, height)); + context.LineTo(new Point(width, height)); + context.EndFigure(true); + } + else + { + var waveX = width * progress; + context.BeginFigure(new Point(0, 0), true); + context.LineTo(new Point(waveX, 0)); + context.CubicBezierTo( + new Point(waveX, waveCenter - bulgeCross), + new Point(waveX + bulgeMain, waveCenter - bulgeCross * 0.5), + new Point(waveX + bulgeMain, waveCenter)); + context.CubicBezierTo( + new Point(waveX + bulgeMain, waveCenter + bulgeCross * 0.5), + new Point(waveX, waveCenter + bulgeCross), + new Point(waveX, height)); + context.LineTo(new Point(0, height)); + context.EndFigure(true); + } + } + else + { + if (forward) + { + var waveY = height * (1 - progress); + context.BeginFigure(new Point(0, height), true); + context.LineTo(new Point(0, waveY)); + context.CubicBezierTo( + new Point(waveCenter - bulgeCross, waveY), + new Point(waveCenter - bulgeCross * 0.5, waveY - bulgeMain), + new Point(waveCenter, waveY - bulgeMain)); + context.CubicBezierTo( + new Point(waveCenter + bulgeCross * 0.5, waveY - bulgeMain), + new Point(waveCenter + bulgeCross, waveY), + new Point(width, waveY)); + context.LineTo(new Point(width, height)); + context.EndFigure(true); + } + else + { + var waveY = height * progress; + context.BeginFigure(new Point(0, 0), true); + context.LineTo(new Point(0, waveY)); + context.CubicBezierTo( + new Point(waveCenter - bulgeCross, waveY), + new Point(waveCenter - bulgeCross * 0.5, waveY + bulgeMain), + new Point(waveCenter, waveY + bulgeMain)); + context.CubicBezierTo( + new Point(waveCenter + bulgeCross * 0.5, waveY + bulgeMain), + new Point(waveCenter + bulgeCross, waveY), + new Point(width, waveY)); + context.LineTo(new Point(width, 0)); + context.EndFigure(true); + } + } + } + + return geometry; + } + } +} diff --git a/samples/ControlCatalog/Pages/WindowCustomizationsPage.xaml b/samples/ControlCatalog/Pages/WindowCustomizationsPage.xaml index a647d34356..df84c0bd6b 100644 --- a/samples/ControlCatalog/Pages/WindowCustomizationsPage.xaml +++ b/samples/ControlCatalog/Pages/WindowCustomizationsPage.xaml @@ -23,22 +23,13 @@ - - - - diff --git a/samples/ControlCatalog/ViewModels/ContextPageViewModel.cs b/samples/ControlCatalog/ViewModels/ContextPageViewModel.cs index f041f32b10..03156f6963 100644 --- a/samples/ControlCatalog/ViewModels/ContextPageViewModel.cs +++ b/samples/ControlCatalog/ViewModels/ContextPageViewModel.cs @@ -49,7 +49,7 @@ namespace ControlCatalog.ViewModels public async Task Open() { - var window = View?.GetVisualRoot() as Window; + var window = TopLevel.GetTopLevel(View) as Window; if (window == null) return; diff --git a/samples/ControlCatalog/ViewModels/MainWindowViewModel.cs b/samples/ControlCatalog/ViewModels/MainWindowViewModel.cs index 716675f2c6..ce9209f61a 100644 --- a/samples/ControlCatalog/ViewModels/MainWindowViewModel.cs +++ b/samples/ControlCatalog/ViewModels/MainWindowViewModel.cs @@ -1,7 +1,6 @@ using Avalonia.Controls; using Avalonia.Controls.ApplicationLifetimes; using Avalonia.Dialogs; -using Avalonia.Platform; using System; using System.ComponentModel.DataAnnotations; using Avalonia; @@ -13,10 +12,7 @@ namespace ControlCatalog.ViewModels { private WindowState _windowState; private WindowState[] _windowStates = Array.Empty(); - private ExtendClientAreaChromeHints _chromeHints = ExtendClientAreaChromeHints.PreferSystemChrome; private bool _extendClientAreaEnabled; - private bool _systemTitleBarEnabled; - private bool _preferSystemChromeEnabled; private double _titleBarHeight; private bool _isSystemBarVisible; private bool _displayEdgeToEdge; @@ -51,65 +47,16 @@ namespace ControlCatalog.ViewModels WindowState.FullScreen, }; - PropertyChanged += (s, e) => - { - if (e.PropertyName is nameof(SystemTitleBarEnabled) or nameof(PreferSystemChromeEnabled)) - { - var hints = ExtendClientAreaChromeHints.NoChrome | ExtendClientAreaChromeHints.OSXThickTitleBar; - - if (SystemTitleBarEnabled) - { - hints |= ExtendClientAreaChromeHints.SystemChrome; - } - if (PreferSystemChromeEnabled) - { - hints |= ExtendClientAreaChromeHints.PreferSystemChrome; - } - ChromeHints = hints; - } - }; - - SystemTitleBarEnabled = true; TitleBarHeight = -1; CanResize = true; CanMinimize = true; CanMaximize = true; } - public ExtendClientAreaChromeHints ChromeHints - { - get { return _chromeHints; } - set { RaiseAndSetIfChanged(ref _chromeHints, value); } - } - public bool ExtendClientAreaEnabled { get { return _extendClientAreaEnabled; } - set - { - if (RaiseAndSetIfChanged(ref _extendClientAreaEnabled, value) && !value) - { - SystemTitleBarEnabled = true; - } - } - } - - public bool SystemTitleBarEnabled - { - get { return _systemTitleBarEnabled; } - set - { - if (RaiseAndSetIfChanged(ref _systemTitleBarEnabled, value) && !value) - { - TitleBarHeight = -1; - } - } - } - - public bool PreferSystemChromeEnabled - { - get { return _preferSystemChromeEnabled; } - set { RaiseAndSetIfChanged(ref _preferSystemChromeEnabled, value); } + set { RaiseAndSetIfChanged(ref _extendClientAreaEnabled, value); } } public double TitleBarHeight diff --git a/samples/ControlCatalog/ViewModels/MenuPageViewModel.cs b/samples/ControlCatalog/ViewModels/MenuPageViewModel.cs index df62ba04cb..f6b406d0d9 100644 --- a/samples/ControlCatalog/ViewModels/MenuPageViewModel.cs +++ b/samples/ControlCatalog/ViewModels/MenuPageViewModel.cs @@ -69,7 +69,7 @@ namespace ControlCatalog.ViewModels public async Task Open() { - var window = View?.GetVisualRoot() as Window; + var window = TopLevel.GetTopLevel(View) as Window; if (window == null) return; var result = await window.StorageProvider.OpenFilePickerAsync(new Avalonia.Platform.Storage.FilePickerOpenOptions() { AllowMultiple = true }); diff --git a/samples/Generators.Sandbox/Generators.Sandbox.csproj b/samples/Generators.Sandbox/Generators.Sandbox.csproj index 5a21bab3e0..2654e1b5b6 100644 --- a/samples/Generators.Sandbox/Generators.Sandbox.csproj +++ b/samples/Generators.Sandbox/Generators.Sandbox.csproj @@ -20,7 +20,7 @@ - + diff --git a/samples/GpuInterop/DrawingSurfaceDemoBase.cs b/samples/GpuInterop/DrawingSurfaceDemoBase.cs index b076f5b489..10d105bca2 100644 --- a/samples/GpuInterop/DrawingSurfaceDemoBase.cs +++ b/samples/GpuInterop/DrawingSurfaceDemoBase.cs @@ -71,12 +71,12 @@ public abstract class DrawingSurfaceDemoBase : Control, IGpuDemo void UpdateFrame() { _updateQueued = false; - var root = this.GetVisualRoot(); - if (root == null) + var source = this.GetPresentationSource(); + if (source == null) return; _visual!.Size = new (Bounds.Width, Bounds.Height); - var size = PixelSize.FromSize(Bounds.Size, root.RenderScaling); + var size = PixelSize.FromSize(Bounds.Size, source.RenderScaling); RenderFrame(size); if (SupportsDisco && Disco > 0) QueueNextFrame(); diff --git a/samples/GpuInterop/GpuInterop.csproj b/samples/GpuInterop/GpuInterop.csproj index d952e60524..54428bce72 100644 --- a/samples/GpuInterop/GpuInterop.csproj +++ b/samples/GpuInterop/GpuInterop.csproj @@ -27,13 +27,13 @@ - - - - - + + + + + - + diff --git a/samples/IntegrationTestApp/IntegrationTestApp.csproj b/samples/IntegrationTestApp/IntegrationTestApp.csproj index 7e45643843..95caa4c64e 100644 --- a/samples/IntegrationTestApp/IntegrationTestApp.csproj +++ b/samples/IntegrationTestApp/IntegrationTestApp.csproj @@ -19,8 +19,8 @@ - - + + diff --git a/samples/IntegrationTestApp/MainWindow.axaml.cs b/samples/IntegrationTestApp/MainWindow.axaml.cs index 894c052d6c..7d6a2c2c3e 100644 --- a/samples/IntegrationTestApp/MainWindow.axaml.cs +++ b/samples/IntegrationTestApp/MainWindow.axaml.cs @@ -5,6 +5,7 @@ using Avalonia.Controls; using IntegrationTestApp.Models; using IntegrationTestApp.Pages; using IntegrationTestApp.ViewModels; +using Page = IntegrationTestApp.Models.Page; namespace IntegrationTestApp { diff --git a/samples/IntegrationTestApp/Pages/GesturesPage.axaml b/samples/IntegrationTestApp/Pages/GesturesPage.axaml index eb028e226f..e171cc62f9 100644 --- a/samples/IntegrationTestApp/Pages/GesturesPage.axaml +++ b/samples/IntegrationTestApp/Pages/GesturesPage.axaml @@ -19,7 +19,7 @@ AutomationProperties.ControlTypeOverride="Image" Tapped="GestureBorder_Tapped" DoubleTapped="GestureBorder_DoubleTapped" - Gestures.RightTapped="GestureBorder_RightTapped"/> + RightTapped="GestureBorder_RightTapped"/> - - - - + diff --git a/samples/IntegrationTestApp/Pages/WindowPage.axaml.cs b/samples/IntegrationTestApp/Pages/WindowPage.axaml.cs index baae2b8766..cc94c4c250 100644 --- a/samples/IntegrationTestApp/Pages/WindowPage.axaml.cs +++ b/samples/IntegrationTestApp/Pages/WindowPage.axaml.cs @@ -51,7 +51,7 @@ public partial class WindowPage : UserControl ShowWindowSize.Text = string.Empty; window.ExtendClientAreaToDecorationsHint = ShowWindowExtendClientAreaToDecorationsHint.IsChecked ?? false; - window.SystemDecorations = (SystemDecorations)ShowWindowSystemDecorations.SelectedIndex; + window.WindowDecorations = (WindowDecorations)ShowWindowSystemDecorations.SelectedIndex; window.WindowState = (WindowState)ShowWindowState.SelectedIndex; switch (ShowWindowMode.SelectedIndex) @@ -87,7 +87,7 @@ public partial class WindowPage : UserControl { Title = "Transparent Window", Name = "TransparentWindow", - SystemDecorations = SystemDecorations.None, + WindowDecorations = WindowDecorations.None, Background = Brushes.Transparent, TransparencyLevelHint = new[] { WindowTransparencyLevel.Transparent }, WindowStartupLocation = WindowStartupLocation.CenterOwner, @@ -136,7 +136,7 @@ public partial class WindowPage : UserControl Width = 200, Height = 200, Background = Brushes.Green, - SystemDecorations = SystemDecorations.None, + WindowDecorations = WindowDecorations.None, WindowStartupLocation = WindowStartupLocation.CenterOwner, Content = new Border { diff --git a/samples/IntegrationTestApp/ShowWindowTest.axaml b/samples/IntegrationTestApp/ShowWindowTest.axaml index 272c61ed0c..38f096e478 100644 --- a/samples/IntegrationTestApp/ShowWindowTest.axaml +++ b/samples/IntegrationTestApp/ShowWindowTest.axaml @@ -45,7 +45,7 @@ - + None BorderOnly Full diff --git a/samples/PlatformSanityChecks/PlatformSanityChecks.csproj b/samples/PlatformSanityChecks/PlatformSanityChecks.csproj index faafd6365f..773813617b 100644 --- a/samples/PlatformSanityChecks/PlatformSanityChecks.csproj +++ b/samples/PlatformSanityChecks/PlatformSanityChecks.csproj @@ -12,5 +12,7 @@ - + + + diff --git a/samples/Previewer/App.xaml b/samples/Previewer/App.xaml deleted file mode 100644 index 817142fefd..0000000000 --- a/samples/Previewer/App.xaml +++ /dev/null @@ -1,5 +0,0 @@ - - - - - diff --git a/samples/Previewer/App.xaml.cs b/samples/Previewer/App.xaml.cs deleted file mode 100644 index ab83d45cd3..0000000000 --- a/samples/Previewer/App.xaml.cs +++ /dev/null @@ -1,22 +0,0 @@ -using Avalonia; -using Avalonia.Controls.ApplicationLifetimes; -using Avalonia.Markup.Xaml; - -namespace Previewer -{ - public class App : Application - { - public override void Initialize() - { - AvaloniaXamlLoader.Load(this); - } - - public override void OnFrameworkInitializationCompleted() - { - if (ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop) - desktop.MainWindow = new MainWindow(); - base.OnFrameworkInitializationCompleted(); - } - } - -} diff --git a/samples/Previewer/Center.cs b/samples/Previewer/Center.cs deleted file mode 100644 index 7a28827d61..0000000000 --- a/samples/Previewer/Center.cs +++ /dev/null @@ -1,19 +0,0 @@ -using Avalonia; -using Avalonia.Controls; - -namespace Previewer -{ - public class Center : Decorator - { - protected override Size ArrangeOverride(Size finalSize) - { - if (Child != null) - { - var desired = Child.DesiredSize; - Child.Arrange(new Rect((finalSize.Width - desired.Width) / 2, (finalSize.Height - desired.Height) / 2, - desired.Width, desired.Height)); - } - return finalSize; - } - } -} \ No newline at end of file diff --git a/samples/Previewer/MainWindow.xaml b/samples/Previewer/MainWindow.xaml deleted file mode 100644 index eb612303f2..0000000000 --- a/samples/Previewer/MainWindow.xaml +++ /dev/null @@ -1,12 +0,0 @@ - - - - - - - - - - - \ No newline at end of file diff --git a/samples/Previewer/MainWindow.xaml.cs b/samples/Previewer/MainWindow.xaml.cs deleted file mode 100644 index dabf90f5d0..0000000000 --- a/samples/Previewer/MainWindow.xaml.cs +++ /dev/null @@ -1,86 +0,0 @@ -using System; -using System.Net; -using Avalonia; -using Avalonia.Controls; -using Avalonia.Controls.Remote; -using Avalonia.Markup.Xaml; -using Avalonia.Remote.Protocol; -using Avalonia.Remote.Protocol.Designer; -using Avalonia.Remote.Protocol.Viewport; -using Avalonia.Threading; - -namespace Previewer -{ - public class MainWindow : Window - { - private const string InitialXaml = @" - Hello world! - - "; - private IAvaloniaRemoteTransportConnection? _connection; - private Control _errorsContainer; - private TextBlock _errors; - private RemoteWidget? _remote; - - - public MainWindow() - { - this.InitializeComponent(); - var tb = this.GetControl("Xaml"); - tb.Text = InitialXaml; - var scroll = this.GetControl("Remote"); - var rem = new Center(); - scroll.Content = rem; - _errorsContainer = this.GetControl("ErrorsContainer"); - _errors = this.GetControl("Errors"); - tb.GetObservable(TextBox.TextProperty).Subscribe(text => _connection?.Send(new UpdateXamlMessage - { - Xaml = text - })); - new BsonTcpTransport().Listen(IPAddress.Loopback, 25000, t => - { - Dispatcher.UIThread.Post(() => - { - if (_connection != null) - { - _connection.Dispose(); - _connection.OnMessage -= OnMessage; - } - _connection = t; - rem.Child = _remote = new RemoteWidget(t); - t.Send(new UpdateXamlMessage - { - Xaml = tb.Text - }); - - t.OnMessage += OnMessage; - }); - }); - Title = "Listening on 127.0.0.1:25000"; - } - - private void OnMessage(IAvaloniaRemoteTransportConnection transport, object obj) - { - Dispatcher.UIThread.Post(() => - { - if (transport != _connection) - return; - if (obj is UpdateXamlResultMessage result) - { - _errorsContainer.IsVisible = result.Error != null; - _errors.Text = result.Error ?? ""; - } - if (obj is RequestViewportResizeMessage resize && _remote is not null) - { - _remote.Width = Math.Min(4096, Math.Max(resize.Width, 1)); - _remote.Height = Math.Min(4096, Math.Max(resize.Height, 1)); - } - }); - } - - private void InitializeComponent() - { - AvaloniaXamlLoader.Load(this); - } - } -} diff --git a/samples/Previewer/Previewer.csproj b/samples/Previewer/Previewer.csproj deleted file mode 100644 index dcc727db8a..0000000000 --- a/samples/Previewer/Previewer.csproj +++ /dev/null @@ -1,20 +0,0 @@ - - - Exe - $(AvsCurrentTargetFramework) - - - - %(Filename) - - - - - - - - - - - - diff --git a/samples/Previewer/Program.cs b/samples/Previewer/Program.cs deleted file mode 100644 index b12b93974a..0000000000 --- a/samples/Previewer/Program.cs +++ /dev/null @@ -1,14 +0,0 @@ -using Avalonia; - -namespace Previewer -{ - class Program - { - public static AppBuilder BuildAvaloniaApp() - => AppBuilder.Configure() - .UsePlatformDetect(); - - public static int Main(string[] args) - => BuildAvaloniaApp().StartWithClassicDesktopLifetime(args); - } -} diff --git a/samples/RemoteDemo/Program.cs b/samples/RemoteDemo/Program.cs deleted file mode 100644 index 0565b676fb..0000000000 --- a/samples/RemoteDemo/Program.cs +++ /dev/null @@ -1,53 +0,0 @@ -using System; -using System.Net; -using System.Net.Sockets; -using System.Threading; -using Avalonia; -using Avalonia.Controls; -using Avalonia.Controls.Remote; -using Avalonia.Remote.Protocol; -using Avalonia.Threading; -using ControlCatalog; - -namespace RemoteDemo -{ - class Program - { - static void Main(string[] args) - { - AppBuilder.Configure().UsePlatformDetect().SetupWithoutStarting(); - - var l = new TcpListener(IPAddress.Loopback, 0); - l.Start(); - var port = ((IPEndPoint) l.LocalEndpoint).Port; - l.Stop(); - - var transport = new BsonTcpTransport(); - transport.Listen(IPAddress.Loopback, port, sc => - { - Dispatcher.UIThread.Post(() => - { - new RemoteServer(sc).Content = new MainView(); - }); - }); - - var cts = new CancellationTokenSource(); - transport.Connect(IPAddress.Loopback, port).ContinueWith(t => - { - Dispatcher.UIThread.Post(() => - { - var window = new Window() - { - Content = new RemoteWidget(t.Result) - }; - window.Closed += delegate { cts.Cancel(); }; - window.Show(); - }); - }); - Dispatcher.UIThread.MainLoop(cts.Token); - - - - } - } -} diff --git a/samples/RemoteDemo/RemoteDemo.csproj b/samples/RemoteDemo/RemoteDemo.csproj deleted file mode 100644 index 8c479b1093..0000000000 --- a/samples/RemoteDemo/RemoteDemo.csproj +++ /dev/null @@ -1,12 +0,0 @@ - - - Exe - $(AvsCurrentTargetFramework) - - - - - - - - diff --git a/samples/RenderDemo/App.config b/samples/RenderDemo/App.config index cd4593817b..86ce1ace90 100644 --- a/samples/RenderDemo/App.config +++ b/samples/RenderDemo/App.config @@ -9,14 +9,6 @@ - - - - - - - - diff --git a/samples/RenderDemo/RenderDemo.csproj b/samples/RenderDemo/RenderDemo.csproj index ef6e46dedd..3603ee0d31 100644 --- a/samples/RenderDemo/RenderDemo.csproj +++ b/samples/RenderDemo/RenderDemo.csproj @@ -17,10 +17,11 @@ + + + - - diff --git a/samples/SampleControls/HamburgerMenu/HamburgerMenu.xaml b/samples/SampleControls/HamburgerMenu/HamburgerMenu.xaml index 4ab09e9584..49972bdc64 100644 --- a/samples/SampleControls/HamburgerMenu/HamburgerMenu.xaml +++ b/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" /> @@ -182,7 +183,9 @@ HorizontalScrollBarVisibility="{TemplateBinding (ScrollViewer.HorizontalScrollBarVisibility)}" VerticalScrollBarVisibility="{TemplateBinding (ScrollViewer.VerticalScrollBarVisibility)}"> + HorizontalAlignment="Stretch" + AutomationProperties.ControlTypeOverride="List" + AutomationProperties.LandmarkType="Navigation"> - + - + @@ -245,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"> diff --git a/samples/XEmbedSample/XEmbedSample.csproj b/samples/XEmbedSample/XEmbedSample.csproj index 2e34bb2cc7..1b51999ada 100644 --- a/samples/XEmbedSample/XEmbedSample.csproj +++ b/samples/XEmbedSample/XEmbedSample.csproj @@ -8,7 +8,7 @@ - + diff --git a/samples/interop/WindowsInteropTest/App.config b/samples/interop/WindowsInteropTest/App.config deleted file mode 100644 index 4fe7131d2a..0000000000 --- a/samples/interop/WindowsInteropTest/App.config +++ /dev/null @@ -1,18 +0,0 @@ - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/samples/interop/WindowsInteropTest/EmbedToWinFormsDemo.Designer.cs b/samples/interop/WindowsInteropTest/EmbedToWinFormsDemo.Designer.cs index d8b0724520..48087a9058 100644 --- a/samples/interop/WindowsInteropTest/EmbedToWinFormsDemo.Designer.cs +++ b/samples/interop/WindowsInteropTest/EmbedToWinFormsDemo.Designer.cs @@ -30,87 +30,88 @@ namespace WindowsInteropTest /// private void InitializeComponent() { - this.button1 = new System.Windows.Forms.Button(); - this.monthCalendar1 = new System.Windows.Forms.MonthCalendar(); - this.groupBox1 = new System.Windows.Forms.GroupBox(); - this.groupBox2 = new System.Windows.Forms.GroupBox(); - this.avaloniaHost = new WinFormsAvaloniaControlHost(); - this.groupBox1.SuspendLayout(); - this.groupBox2.SuspendLayout(); - this.SuspendLayout(); - // - // button1 - // - this.button1.Location = new System.Drawing.Point(28, 29); - this.button1.Name = "button1"; - this.button1.Size = new System.Drawing.Size(164, 73); - this.button1.TabIndex = 0; - this.button1.Text = "button1"; - this.button1.UseVisualStyleBackColor = true; - // + OpenWindowButton = new System.Windows.Forms.Button(); + monthCalendar1 = new System.Windows.Forms.MonthCalendar(); + groupBox1 = new System.Windows.Forms.GroupBox(); + groupBox2 = new System.Windows.Forms.GroupBox(); + avaloniaHost = new Avalonia.Win32.Interoperability.WinFormsAvaloniaControlHost(); + groupBox1.SuspendLayout(); + groupBox2.SuspendLayout(); + SuspendLayout(); + // + // OpenWindowButton + // + OpenWindowButton.Location = new System.Drawing.Point(33, 33); + OpenWindowButton.Margin = new System.Windows.Forms.Padding(4, 3, 4, 3); + OpenWindowButton.Name = "OpenWindowButton"; + OpenWindowButton.Size = new System.Drawing.Size(191, 84); + OpenWindowButton.TabIndex = 0; + OpenWindowButton.Text = "Open Avalonia Window"; + OpenWindowButton.UseVisualStyleBackColor = true; + OpenWindowButton.Click += OpenWindowButton_Click; + // // monthCalendar1 - // - this.monthCalendar1.Location = new System.Drawing.Point(28, 114); - this.monthCalendar1.Name = "monthCalendar1"; - this.monthCalendar1.TabIndex = 1; - // + // + monthCalendar1.Location = new System.Drawing.Point(33, 132); + monthCalendar1.Margin = new System.Windows.Forms.Padding(10); + monthCalendar1.Name = "monthCalendar1"; + monthCalendar1.TabIndex = 1; + // // groupBox1 - // - this.groupBox1.Anchor = ((System.Windows.Forms.AnchorStyles)(((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Bottom) - | System.Windows.Forms.AnchorStyles.Left))); - this.groupBox1.Controls.Add(this.button1); - this.groupBox1.Controls.Add(this.monthCalendar1); - this.groupBox1.Location = new System.Drawing.Point(12, 12); - this.groupBox1.Name = "groupBox1"; - this.groupBox1.Size = new System.Drawing.Size(227, 418); - this.groupBox1.TabIndex = 2; - this.groupBox1.TabStop = false; - this.groupBox1.Text = "WinForms"; - // + // + groupBox1.Anchor = ((System.Windows.Forms.AnchorStyles)((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Bottom) | System.Windows.Forms.AnchorStyles.Left)); + groupBox1.Controls.Add(OpenWindowButton); + groupBox1.Controls.Add(monthCalendar1); + groupBox1.Location = new System.Drawing.Point(14, 14); + groupBox1.Margin = new System.Windows.Forms.Padding(4, 3, 4, 3); + groupBox1.Name = "groupBox1"; + groupBox1.Padding = new System.Windows.Forms.Padding(4, 3, 4, 3); + groupBox1.Size = new System.Drawing.Size(265, 482); + groupBox1.TabIndex = 2; + groupBox1.TabStop = false; + groupBox1.Text = "WinForms"; + // // groupBox2 - // - this.groupBox2.Anchor = ((System.Windows.Forms.AnchorStyles)((((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Bottom) - | System.Windows.Forms.AnchorStyles.Left) - | System.Windows.Forms.AnchorStyles.Right))); - this.groupBox2.Controls.Add(this.avaloniaHost); - this.groupBox2.Location = new System.Drawing.Point(245, 12); - this.groupBox2.Name = "groupBox2"; - this.groupBox2.Size = new System.Drawing.Size(501, 418); - this.groupBox2.TabIndex = 3; - this.groupBox2.TabStop = false; - this.groupBox2.Text = "Avalonia"; - // + // + groupBox2.Anchor = ((System.Windows.Forms.AnchorStyles)(((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Bottom) | System.Windows.Forms.AnchorStyles.Left) | System.Windows.Forms.AnchorStyles.Right)); + groupBox2.Controls.Add(avaloniaHost); + groupBox2.Location = new System.Drawing.Point(286, 14); + groupBox2.Margin = new System.Windows.Forms.Padding(4, 3, 4, 3); + groupBox2.Name = "groupBox2"; + groupBox2.Padding = new System.Windows.Forms.Padding(4, 3, 4, 3); + groupBox2.Size = new System.Drawing.Size(584, 482); + groupBox2.TabIndex = 3; + groupBox2.TabStop = false; + groupBox2.Text = "Avalonia"; + // // avaloniaHost - // - this.avaloniaHost.Anchor = ((System.Windows.Forms.AnchorStyles)((((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Bottom) - | System.Windows.Forms.AnchorStyles.Left) - | System.Windows.Forms.AnchorStyles.Right))); - this.avaloniaHost.Content = null; - this.avaloniaHost.Location = new System.Drawing.Point(6, 19); - this.avaloniaHost.Name = "avaloniaHost"; - this.avaloniaHost.Size = new System.Drawing.Size(489, 393); - this.avaloniaHost.TabIndex = 0; - this.avaloniaHost.Text = "avaloniaHost"; - // + // + avaloniaHost.Anchor = ((System.Windows.Forms.AnchorStyles)(((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Bottom) | System.Windows.Forms.AnchorStyles.Left) | System.Windows.Forms.AnchorStyles.Right)); + avaloniaHost.Location = new System.Drawing.Point(7, 22); + avaloniaHost.Margin = new System.Windows.Forms.Padding(4, 3, 4, 3); + avaloniaHost.Name = "avaloniaHost"; + avaloniaHost.Size = new System.Drawing.Size(570, 453); + avaloniaHost.TabIndex = 0; + avaloniaHost.Text = "avaloniaHost"; + // // EmbedToWinFormsDemo - // - this.AutoScaleDimensions = new System.Drawing.SizeF(6F, 13F); - this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font; - this.ClientSize = new System.Drawing.Size(758, 442); - this.Controls.Add(this.groupBox2); - this.Controls.Add(this.groupBox1); - this.MinimumSize = new System.Drawing.Size(600, 400); - this.Name = "EmbedToWinFormsDemo"; - this.Text = "EmbedToWinFormsDemo"; - this.groupBox1.ResumeLayout(false); - this.groupBox2.ResumeLayout(false); - this.ResumeLayout(false); - + // + AutoScaleDimensions = new System.Drawing.SizeF(7F, 15F); + AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font; + ClientSize = new System.Drawing.Size(884, 510); + Controls.Add(groupBox2); + Controls.Add(groupBox1); + Margin = new System.Windows.Forms.Padding(4, 3, 4, 3); + MinimumSize = new System.Drawing.Size(697, 456); + Text = "EmbedToWinFormsDemo"; + groupBox1.ResumeLayout(false); + groupBox2.ResumeLayout(false); + ResumeLayout(false); } #endregion - private System.Windows.Forms.Button button1; + private System.Windows.Forms.Button OpenWindowButton; private System.Windows.Forms.MonthCalendar monthCalendar1; private System.Windows.Forms.GroupBox groupBox1; private System.Windows.Forms.GroupBox groupBox2; diff --git a/samples/interop/WindowsInteropTest/EmbedToWinFormsDemo.cs b/samples/interop/WindowsInteropTest/EmbedToWinFormsDemo.cs index d37ed13559..69dfcb1bbc 100644 --- a/samples/interop/WindowsInteropTest/EmbedToWinFormsDemo.cs +++ b/samples/interop/WindowsInteropTest/EmbedToWinFormsDemo.cs @@ -1,5 +1,10 @@ -using System.Windows.Forms; +using System; +using System.Windows.Forms; using ControlCatalog; +using AvaloniaButton = Avalonia.Controls.Button; +using AvaloniaStackPanel = Avalonia.Controls.StackPanel; +using AvaloniaTextBox = Avalonia.Controls.TextBox; +using AvaloniaWindow = Avalonia.Controls.Window; namespace WindowsInteropTest { @@ -10,5 +15,23 @@ namespace WindowsInteropTest InitializeComponent(); avaloniaHost.Content = new MainView(); } + + private void OpenWindowButton_Click(object sender, EventArgs e) + { + var window = new AvaloniaWindow + { + Width = 300, + Height = 300, + Content = new AvaloniaStackPanel + { + Children = + { + new AvaloniaButton { Content = "Button" }, + new AvaloniaTextBox { Text = "Text" } + } + } + }; + window.Show(); + } } } diff --git a/samples/interop/WindowsInteropTest/Program.cs b/samples/interop/WindowsInteropTest/Program.cs index 4ebb88642b..8ef01523d9 100644 --- a/samples/interop/WindowsInteropTest/Program.cs +++ b/samples/interop/WindowsInteropTest/Program.cs @@ -1,6 +1,7 @@ using System; using ControlCatalog; using Avalonia; +using Avalonia.Win32.Interoperability; namespace WindowsInteropTest { @@ -14,9 +15,11 @@ namespace WindowsInteropTest { System.Windows.Forms.Application.EnableVisualStyles(); System.Windows.Forms.Application.SetCompatibleTextRenderingDefault(false); + System.Windows.Forms.Application.AddMessageFilter(new WinFormsAvaloniaMessageFilter()); AppBuilder.Configure() .UseWin32() .UseSkia() + .UseHarfBuzz() .SetupWithoutStarting(); System.Windows.Forms.Application.Run(new EmbedToWinFormsDemo()); } diff --git a/samples/interop/WindowsInteropTest/WindowsInteropTest.csproj b/samples/interop/WindowsInteropTest/WindowsInteropTest.csproj index 576910ca3d..e282d93121 100644 --- a/samples/interop/WindowsInteropTest/WindowsInteropTest.csproj +++ b/samples/interop/WindowsInteropTest/WindowsInteropTest.csproj @@ -8,6 +8,7 @@ + diff --git a/src/Android/Avalonia.Android/AndroidPlatform.cs b/src/Android/Avalonia.Android/AndroidPlatform.cs index 5316a84570..460aaec5ca 100644 --- a/src/Android/Avalonia.Android/AndroidPlatform.cs +++ b/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() ?? new AndroidPlatformOptions(); + Dispatcher.InitializeUIThreadDispatcher(new AndroidDispatcherImpl()); + Timer = new ChoreographerTimer(); AvaloniaLocator.CurrentMutable .Bind().ToTransient() .Bind().ToConstant(new WindowingPlatformStub()) .Bind().ToSingleton() .Bind().ToSingleton() - .Bind().ToConstant(new AndroidDispatcherImpl()) .Bind().ToSingleton() - .Bind().ToConstant(new ChoreographerTimer()) + .Bind().ToConstant(RenderLoop.FromTimer(Timer)) .Bind().ToSingleton() .Bind().ToConstant(new KeyGestureFormatInfo(new Dictionary() { })) .Bind().ToConstant(new AndroidActivatableLifetime()); diff --git a/src/Android/Avalonia.Android/Avalonia.Android.csproj b/src/Android/Avalonia.Android/Avalonia.Android.csproj index 0c1100ab5f..befdcbcd5e 100644 --- a/src/Android/Avalonia.Android/Avalonia.Android.csproj +++ b/src/Android/Avalonia.Android/Avalonia.Android.csproj @@ -10,8 +10,8 @@ - - + + diff --git a/src/Android/Avalonia.Android/AvaloniaView.cs b/src/Android/Avalonia.Android/AvaloniaView.cs index 86b96772ce..d8df486bb3 100644 --- a/src/Android/Avalonia.Android/AvaloniaView.cs +++ b/src/Android/Avalonia.Android/AvaloniaView.cs @@ -100,7 +100,7 @@ namespace Avalonia.Android return; if (isVisible && _timerSubscription == null) { - if (AvaloniaLocator.Current.GetService() is ChoreographerTimer timer) + if (AndroidPlatform.Timer is { } timer) { _timerSubscription = timer.SubscribeView(this); } diff --git a/src/Android/Avalonia.Android/ChoreographerTimer.cs b/src/Android/Avalonia.Android/ChoreographerTimer.cs index adca9c72ce..9bc8e78a52 100644 --- a/src/Android/Avalonia.Android/ChoreographerTimer.cs +++ b/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 _views = new(); - private Action? _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 Tick + public Action? 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(); } diff --git a/src/Android/Avalonia.Android/CursorFactory.cs b/src/Android/Avalonia.Android/CursorFactory.cs index 6293637d4e..e60524c2b7 100644 --- a/src/Android/Avalonia.Android/CursorFactory.cs +++ b/src/Android/Avalonia.Android/CursorFactory.cs @@ -5,7 +5,7 @@ namespace Avalonia.Android { internal class CursorFactory : ICursorFactory { - public ICursorImpl CreateCursor(IBitmapImpl cursor, PixelPoint hotSpot) => CursorImpl.ZeroCursor; + public ICursorImpl CreateCursor(Avalonia.Media.Imaging.Bitmap cursor, PixelPoint hotSpot) => CursorImpl.ZeroCursor; public ICursorImpl GetCursor(StandardCursorType cursorType) => CursorImpl.ZeroCursor; diff --git a/src/Android/Avalonia.Android/Platform/Input/AvaloniaInputConnection.cs b/src/Android/Avalonia.Android/Platform/Input/AvaloniaInputConnection.cs index 39e1574901..a93803db09 100644 --- a/src/Android/Avalonia.Android/Platform/Input/AvaloniaInputConnection.cs +++ b/src/Android/Avalonia.Android/Platform/Input/AvaloniaInputConnection.cs @@ -157,8 +157,7 @@ namespace Avalonia.Android.Platform.Input } case ImeAction.Next: { - FocusManager.GetFocusManager(_toplevel.InputRoot)? - .TryMoveFocus(NavigationDirection.Next); + ((FocusManager?)_toplevel.InputRoot?.FocusManager)?.TryMoveFocus(NavigationDirection.Next); break; } } diff --git a/src/Android/Avalonia.Android/Platform/SkiaPlatform/FramebufferManager.cs b/src/Android/Avalonia.Android/Platform/SkiaPlatform/FramebufferManager.cs index e096d32f48..596f09fda3 100644 --- a/src/Android/Avalonia.Android/Platform/SkiaPlatform/FramebufferManager.cs +++ b/src/Android/Avalonia.Android/Platform/SkiaPlatform/FramebufferManager.cs @@ -1,5 +1,5 @@ using System; -using Avalonia.Controls.Platform.Surfaces; +using Avalonia.Platform.Surfaces; using Avalonia.Platform; namespace Avalonia.Android.Platform.SkiaPlatform diff --git a/src/Android/Avalonia.Android/Platform/SkiaPlatform/TopLevelImpl.cs b/src/Android/Avalonia.Android/Platform/SkiaPlatform/TopLevelImpl.cs index b11d35d1ef..20284906be 100644 --- a/src/Android/Avalonia.Android/Platform/SkiaPlatform/TopLevelImpl.cs +++ b/src/Android/Avalonia.Android/Platform/SkiaPlatform/TopLevelImpl.cs @@ -18,6 +18,7 @@ using Avalonia.Input.Raw; using Avalonia.Input.TextInput; using Avalonia.OpenGL.Egl; using Avalonia.Platform; +using Avalonia.Platform.Surfaces; using Avalonia.Platform.Storage; using Avalonia.Rendering.Composition; using Java.Lang; @@ -96,7 +97,7 @@ namespace Avalonia.Android.Platform.SkiaPlatform public double DesktopScaling => RenderScaling; public IPlatformHandle Handle { get; } - public IEnumerable Surfaces { get; } + public IPlatformRenderSurface[] Surfaces { get; } public Compositor Compositor => AndroidPlatform.Compositor ?? throw new InvalidOperationException("Android backend wasn't initialized. Make sure .UseAndroid() was executed."); diff --git a/src/Android/Avalonia.Android/Platform/Storage/AndroidStorageItem.cs b/src/Android/Avalonia.Android/Platform/Storage/AndroidStorageItem.cs index d8cd173183..59ec332b2d 100644 --- a/src/Android/Avalonia.Android/Platform/Storage/AndroidStorageItem.cs +++ b/src/Android/Avalonia.Android/Platform/Storage/AndroidStorageItem.cs @@ -12,6 +12,7 @@ using Avalonia.Logging; using Avalonia.Platform.Storage; using Avalonia.Platform.Storage.FileIO; using Java.Lang; +using static Android.Provider.DocumentsContract; using AndroidUri = Android.Net.Uri; using Exception = System.Exception; using JavaFile = Java.IO.File; @@ -35,10 +36,10 @@ internal abstract class AndroidStorageItem : IStorageBookmarkItem } internal AndroidUri Uri { get; set; } - + protected Activity Activity => _activity ?? throw new ObjectDisposedException(nameof(AndroidStorageItem)); - public virtual string Name => GetColumnValue(Activity, Uri, DocumentsContract.Document.ColumnDisplayName) + public virtual string Name => GetColumnValue(Activity, Uri, Document.ColumnDisplayName) ?? GetColumnValue(Activity, Uri, MediaStore.IMediaColumns.DisplayName) ?? Uri.PathSegments?.LastOrDefault()?.Split("/", StringSplitOptions.RemoveEmptyEntries).LastOrDefault() ?? string.Empty; @@ -67,7 +68,7 @@ internal abstract class AndroidStorageItem : IStorageBookmarkItem Activity.ContentResolver?.ReleasePersistableUriPermission(Uri, ActivityFlags.GrantWriteUriPermission | ActivityFlags.GrantReadUriPermission); } - + public abstract Task GetBasicPropertiesAsync(); protected static string? GetColumnValue(Context context, AndroidUri contentUri, string column, string? selection = null, string[]? selectionArgs = null) @@ -98,7 +99,7 @@ internal abstract class AndroidStorageItem : IStorageBookmarkItem return null; } - if(_parent != null) + if (_parent != null) { return _parent; } @@ -106,8 +107,8 @@ internal abstract class AndroidStorageItem : IStorageBookmarkItem using var javaFile = new JavaFile(Uri.Path!); // Java file represents files AND directories. Don't be confused. - if (javaFile.ParentFile is {} parentFile - && AndroidUri.FromFile(parentFile) is {} androidUri) + if (javaFile.ParentFile is { } parentFile + && AndroidUri.FromFile(parentFile) is { } androidUri) { return new AndroidStorageFolder(Activity, androidUri, false); } @@ -124,12 +125,12 @@ internal abstract class AndroidStorageItem : IStorageBookmarkItem return await _activity!.CheckPermission(Manifest.Permission.ReadExternalStorage); } - + public void Dispose() { _activity = null; } - + internal AndroidUri? PermissionRoot => _permissionRoot; public abstract Task DeleteAsync(); @@ -138,8 +139,8 @@ internal abstract class AndroidStorageItem : IStorageBookmarkItem public static IStorageItem CreateItem(Activity activity, AndroidUri uri) { - var mimeType = GetColumnValue(activity, uri, DocumentsContract.Document.ColumnMimeType); - if (mimeType == DocumentsContract.Document.MimeTypeDir) + var mimeType = GetColumnValue(activity, uri, Document.ColumnMimeType); + if (mimeType == Document.MimeTypeDir) { return new AndroidStorageFolder(activity, uri, false); } @@ -159,9 +160,9 @@ internal class AndroidStorageFolder : AndroidStorageItem, IStorageBookmarkFolder public Task CreateFileAsync(string name) { var mimeType = MimeTypeMap.Singleton?.GetMimeTypeFromExtension(MimeTypeMap.GetFileExtensionFromUrl(name)) ?? "application/octet-stream"; - var treeUri = DocumentsContract.BuildDocumentUriUsingTree(Uri, DocumentsContract.GetTreeDocumentId(Uri)); - var newFile = DocumentsContract.CreateDocument(Activity.ContentResolver!, treeUri!, mimeType, name); - if(newFile == null) + var treeUri = GetTreeUri().treeUri; + var newFile = CreateDocument(Activity.ContentResolver!, treeUri!, mimeType, name); + if (newFile == null) { return Task.FromResult(null); } @@ -171,8 +172,8 @@ internal class AndroidStorageFolder : AndroidStorageItem, IStorageBookmarkFolder public Task CreateFolderAsync(string name) { - var treeUri = DocumentsContract.BuildDocumentUriUsingTree(Uri, DocumentsContract.GetTreeDocumentId(Uri)); - var newFolder = DocumentsContract.CreateDocument(Activity.ContentResolver!, treeUri!, DocumentsContract.Document.MimeTypeDir, name); + var treeUri = GetTreeUri().treeUri; + var newFolder = CreateDocument(Activity.ContentResolver!, treeUri!, Document.MimeTypeDir, name); if (newFolder == null) { return Task.FromResult(null); @@ -197,24 +198,76 @@ internal class AndroidStorageFolder : AndroidStorageItem, IStorageBookmarkFolder { await foreach (var file in storageFolder.GetItemsAsync()) { - if(file is AndroidStorageFolder folder) + if (file is AndroidStorageFolder folder) { await DeleteContents(folder); } - else if(file is AndroidStorageFile storageFile) + else if (file is AndroidStorageFile storageFile) { await storageFile.DeleteAsync(); } } - var treeUri = DocumentsContract.BuildDocumentUriUsingTree(storageFolder.Uri, DocumentsContract.GetTreeDocumentId(storageFolder.Uri)); - DocumentsContract.DeleteDocument(Activity.ContentResolver!, treeUri!); + var treeUri = GetTreeUri().treeUri; + DeleteDocument(Activity.ContentResolver!, treeUri!); } } public override Task GetBasicPropertiesAsync() { - return Task.FromResult(new StorageItemProperties()); + DateTimeOffset? dateModified = null; + + AndroidUri? queryUri = null; + + try + { + try + { + // When Uri is a tree URI, use its document id to build a document URI. + var folderId = GetTreeDocumentId(Uri); + queryUri = BuildDocumentUriUsingTree(Uri, folderId); + } + catch (UnsupportedOperationException) + { + // For non-root items, Uri may already be a document URI; use it directly. + queryUri = Uri; + } + + if (queryUri != null) + { + var projection = new[] + { + Document.ColumnLastModified + }; + using var cursor = Activity.ContentResolver!.Query(queryUri, projection, null, null, null); + + if (cursor?.MoveToFirst() == true) + { + try + { + var columnIndex = cursor.GetColumnIndex(Document.ColumnLastModified); + if (columnIndex != -1) + { + var longValue = cursor.GetLong(columnIndex); + dateModified = longValue > 0 ? DateTimeOffset.FromUnixTimeMilliseconds(longValue) : null; + } + } + catch (Exception ex) + { + Logger.TryGet(LogEventLevel.Verbose, LogArea.AndroidPlatform)? + .Log(this, "Directory LastModified metadata reader failed: '{Exception}'", ex); + } + } + } + } + catch (Exception ex) + { + // Data may not be available for this item or the URI may not be in the expected shape. + Logger.TryGet(LogEventLevel.Verbose, LogArea.AndroidPlatform)? + .Log(this, "Directory basic properties metadata unavailable: '{Exception}'", ex); + } + + return Task.FromResult(new StorageItemProperties(null, null, dateModified)); } public async IAsyncEnumerable GetItemsAsync() @@ -230,14 +283,12 @@ internal class AndroidStorageFolder : AndroidStorageItem, IStorageBookmarkFolder yield break; } - var root = PermissionRoot ?? Uri; - var folderId = root != Uri ? DocumentsContract.GetDocumentId(Uri) : DocumentsContract.GetTreeDocumentId(Uri); - var childrenUri = DocumentsContract.BuildChildDocumentsUriUsingTree(root, folderId); + var (root, childrenUri) = GetTreeUri(); var projection = new[] { - DocumentsContract.Document.ColumnDocumentId, - DocumentsContract.Document.ColumnMimeType + Document.ColumnDocumentId, + Document.ColumnMimeType }; if (childrenUri != null) { @@ -249,8 +300,8 @@ internal class AndroidStorageFolder : AndroidStorageItem, IStorageBookmarkFolder var mime = cursor.GetString(1); var id = cursor.GetString(0); - bool isDirectory = mime == DocumentsContract.Document.MimeTypeDir; - var uri = DocumentsContract.BuildDocumentUriUsingTree(root, id); + bool isDirectory = mime == Document.MimeTypeDir; + var uri = BuildDocumentUriUsingTree(root, id); if (uri == null) { @@ -311,15 +362,13 @@ internal class AndroidStorageFolder : AndroidStorageItem, IStorageBookmarkFolder return null; } - var root = PermissionRoot ?? Uri; - var folderId = root != Uri ? DocumentsContract.GetDocumentId(Uri) : DocumentsContract.GetTreeDocumentId(Uri); - var childrenUri = DocumentsContract.BuildChildDocumentsUriUsingTree(root, folderId); + var (root, childrenUri) = GetTreeUri(); var projection = new[] { - DocumentsContract.Document.ColumnDocumentId, - DocumentsContract.Document.ColumnMimeType, - DocumentsContract.Document.ColumnDisplayName + Document.ColumnDocumentId, + Document.ColumnMimeType, + Document.ColumnDisplayName }; if (childrenUri != null) @@ -336,15 +385,15 @@ internal class AndroidStorageFolder : AndroidStorageItem, IStorageBookmarkFolder if (fileName != name) { continue; - } + } - bool mineDirectory = mime == DocumentsContract.Document.MimeTypeDir; + bool mineDirectory = mime == Document.MimeTypeDir; if (isDirectory != mineDirectory) { return null; } - var uri = DocumentsContract.BuildDocumentUriUsingTree(root, id); + var uri = BuildDocumentUriUsingTree(root, id); if (uri == null) { return null; @@ -370,6 +419,13 @@ internal class AndroidStorageFolder : AndroidStorageItem, IStorageBookmarkFolder var file = await GetItemAsync(name, false); return (IStorageFile?)file; } + + private (AndroidUri root, AndroidUri? treeUri) GetTreeUri() + { + var root = PermissionRoot ?? Uri; + var folderId = root != Uri ? GetDocumentId(Uri) : GetTreeDocumentId(Uri); + return (root, BuildChildDocumentsUriUsingTree(root, folderId)); + } } internal sealed class WellKnownAndroidStorageFolder : AndroidStorageFolder @@ -407,7 +463,7 @@ internal sealed class AndroidStorageFile : AndroidStorageItem, IStorageBookmarkF } return isOutput - ? context.ContentResolver?.OpenOutputStream(uri) + ? context.ContentResolver?.OpenOutputStream(uri, "wt") : context.ContentResolver?.OpenInputStream(uri); } @@ -416,10 +472,10 @@ internal sealed class AndroidStorageFile : AndroidStorageItem, IStorageBookmarkF if (!OperatingSystem.IsAndroidVersionAtLeast(24)) return false; - if (!DocumentsContract.IsDocumentUri(context, uri)) + if (!IsDocumentUri(context, uri)) return false; - var value = GetColumnValue(context, uri, DocumentsContract.Document.ColumnFlags); + var value = GetColumnValue(context, uri, Document.ColumnFlags); if (!string.IsNullOrEmpty(value) && int.TryParse(value, out var flagsInt)) { var flags = (DocumentContractFlags)flagsInt; @@ -527,7 +583,7 @@ internal sealed class AndroidStorageFile : AndroidStorageItem, IStorageBookmarkF if (Activity != null) { - DocumentsContract.DeleteDocument(Activity.ContentResolver!, Uri); + DeleteDocument(Activity.ContentResolver!, Uri); } } @@ -550,7 +606,7 @@ internal sealed class AndroidStorageFile : AndroidStorageItem, IStorageBookmarkF storageFolder.Uri is { } targetParentUri && await GetParentAsync() is AndroidStorageFolder parentFolder) { - movedUri = DocumentsContract.MoveDocument(contentResolver, Uri, parentFolder.Uri, targetParentUri); + movedUri = MoveDocument(contentResolver, Uri, parentFolder.Uri, targetParentUri); } } catch (Exception) diff --git a/src/Android/Avalonia.Android/Platform/Vulkan/VulkanSupport.cs b/src/Android/Avalonia.Android/Platform/Vulkan/VulkanSupport.cs index c1abaa05a5..d8e17e4330 100644 --- a/src/Android/Avalonia.Android/Platform/Vulkan/VulkanSupport.cs +++ b/src/Android/Avalonia.Android/Platform/Vulkan/VulkanSupport.cs @@ -2,6 +2,7 @@ using System.Collections.Generic; using System.Runtime.InteropServices; using Avalonia.Platform; +using Avalonia.Platform.Surfaces; using Avalonia.Vulkan; namespace Avalonia.Android.Platform.Vulkan @@ -24,10 +25,10 @@ namespace Avalonia.Android.Platform.Vulkan internal class VulkanSurfaceFactory : IVulkanKhrSurfacePlatformSurfaceFactory { - public bool CanRenderToSurface(IVulkanPlatformGraphicsContext context, object surface) => + public bool CanRenderToSurface(IVulkanPlatformGraphicsContext context, IPlatformRenderSurface surface) => surface is INativePlatformHandleSurface handle; - public IVulkanKhrSurfacePlatformSurface CreateSurface(IVulkanPlatformGraphicsContext context, object handle) => + public IVulkanKhrSurfacePlatformSurface CreateSurface(IVulkanPlatformGraphicsContext context, IPlatformRenderSurface handle) => new AndroidVulkanSurface((INativePlatformHandleSurface)handle); } diff --git a/src/Avalonia.Base/Animation/AnimationInstance`1.cs b/src/Avalonia.Base/Animation/AnimationInstance`1.cs index 3be646d66c..6358dd2c6b 100644 --- a/src/Avalonia.Base/Animation/AnimationInstance`1.cs +++ b/src/Avalonia.Base/Animation/AnimationInstance`1.cs @@ -7,7 +7,7 @@ using Avalonia.Data; namespace Avalonia.Animation { /// - /// Handles interpolation and time-related functions + /// Handles interpolation and time-related functions /// for keyframe animations. /// internal class AnimationInstance : SingleSubscriberObservableBase @@ -35,6 +35,8 @@ namespace Avalonia.Animation private readonly IClock _baseClock; private IClock? _clock; private EventHandler? _propertyChangedDelegate; + private EventHandler? _visibilityChangedHandler; + private EventHandler? _detachedHandler; public AnimationInstance(Animation animation, Animatable control, Animator animator, IClock baseClock, Action? OnComplete, Func 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) { diff --git a/src/Avalonia.Base/Animation/CompositePageTransition.cs b/src/Avalonia.Base/Animation/CompositePageTransition.cs index 62119a0051..e5e3511337 100644 --- a/src/Avalonia.Base/Animation/CompositePageTransition.cs +++ b/src/Avalonia.Base/Animation/CompositePageTransition.cs @@ -28,7 +28,7 @@ namespace Avalonia.Animation /// /// /// - public class CompositePageTransition : IPageTransition + public class CompositePageTransition : IPageTransition, IProgressPageTransition { /// /// Gets or sets the transitions to be executed. Can be defined from XAML. @@ -44,5 +44,35 @@ namespace Avalonia.Animation .ToArray(); return Task.WhenAll(transitionTasks); } + + /// + public void Update( + double progress, + Visual? from, + Visual? to, + bool forward, + double pageLength, + IReadOnlyList visibleItems) + { + foreach (var transition in PageTransitions) + { + if (transition is IProgressPageTransition progressive) + { + progressive.Update(progress, from, to, forward, pageLength, visibleItems); + } + } + } + + /// + public void Reset(Visual visual) + { + foreach (var transition in PageTransitions) + { + if (transition is IProgressPageTransition progressive) + { + progressive.Reset(visual); + } + } + } } } diff --git a/src/Avalonia.Base/Animation/CrossFade.cs b/src/Avalonia.Base/Animation/CrossFade.cs index 99b34e1ff0..45a4300e5b 100644 --- a/src/Avalonia.Base/Animation/CrossFade.cs +++ b/src/Avalonia.Base/Animation/CrossFade.cs @@ -12,8 +12,13 @@ namespace Avalonia.Animation /// /// Defines a cross-fade animation between two s. /// - public class CrossFade : IPageTransition + public class CrossFade : IPageTransition, IProgressPageTransition { + private const double SidePeekOpacity = 0.72; + private const double FarPeekOpacity = 0.42; + private const double OutgoingDip = 0.22; + private const double IncomingBoost = 0.12; + private const double PassiveDip = 0.05; private readonly Animation _fadeOutAnimation; private readonly Animation _fadeInAnimation; @@ -33,6 +38,7 @@ namespace Avalonia.Animation { _fadeOutAnimation = new Animation { + FillMode = FillMode.Forward, Children = { new KeyFrame() @@ -64,6 +70,7 @@ namespace Avalonia.Animation }; _fadeInAnimation = new Animation { + FillMode = FillMode.Forward, Children = { new KeyFrame() @@ -123,6 +130,16 @@ namespace Avalonia.Animation set => _fadeOutAnimation.Easing = value; } + /// + /// Gets or sets the fill mode applied to both fade animations. + /// Defaults to . + /// + public FillMode FillMode + { + get => _fadeOutAnimation.FillMode; + set => _fadeOutAnimation.FillMode = _fadeInAnimation.FillMode = value; + } + /// public async Task Start(Visual? from, Visual? to, CancellationToken cancellationToken) { @@ -147,9 +164,7 @@ namespace Avalonia.Animation await Task.WhenAll(tasks); if (from != null && !cancellationToken.IsCancellationRequested) - { from.IsVisible = false; - } } /// @@ -172,5 +187,82 @@ namespace Avalonia.Animation { return Start(from, to, cancellationToken); } + + /// + public void Update( + double progress, + Visual? from, + Visual? to, + bool forward, + double pageLength, + IReadOnlyList visibleItems) + { + if (visibleItems.Count > 0) + { + UpdateVisibleItems(progress, from, to, visibleItems); + return; + } + + if (from != null) + from.Opacity = 1 - progress; + if (to != null) + { + to.IsVisible = true; + to.Opacity = progress; + } + } + + /// + public void Reset(Visual visual) + { + visual.Opacity = 1; + } + + private static void UpdateVisibleItems( + double progress, + Visual? from, + Visual? to, + IReadOnlyList visibleItems) + { + var emphasis = Math.Sin(Math.Clamp(progress, 0.0, 1.0) * Math.PI); + foreach (var item in visibleItems) + { + item.Visual.IsVisible = true; + var opacity = GetOpacityForOffset(item.ViewportCenterOffset); + + if (ReferenceEquals(item.Visual, from)) + { + opacity = Math.Max(FarPeekOpacity, opacity - (OutgoingDip * emphasis)); + } + else if (ReferenceEquals(item.Visual, to)) + { + opacity = Math.Min(1.0, opacity + (IncomingBoost * emphasis)); + } + else + { + opacity = Math.Max(FarPeekOpacity, opacity - (PassiveDip * emphasis)); + } + + item.Visual.Opacity = opacity; + } + } + + private static double GetOpacityForOffset(double offsetFromCenter) + { + var distance = Math.Abs(offsetFromCenter); + + if (distance <= 1.0) + return Lerp(1.0, SidePeekOpacity, distance); + + if (distance <= 2.0) + return Lerp(SidePeekOpacity, FarPeekOpacity, distance - 1.0); + + return FarPeekOpacity; + } + + private static double Lerp(double from, double to, double t) + { + return from + ((to - from) * Math.Clamp(t, 0.0, 1.0)); + } } } diff --git a/src/Avalonia.Base/Animation/IProgressPageTransition.cs b/src/Avalonia.Base/Animation/IProgressPageTransition.cs new file mode 100644 index 0000000000..01f892d1fd --- /dev/null +++ b/src/Avalonia.Base/Animation/IProgressPageTransition.cs @@ -0,0 +1,39 @@ +using System.Collections.Generic; +using Avalonia.VisualTree; + +namespace Avalonia.Animation +{ + /// + /// An that supports progress-driven updates. + /// + /// + /// Transitions implementing this interface can be driven by a normalized progress value + /// (0.0 to 1.0) during swipe gestures or programmatic animations, rather than running + /// as a timed animation via . + /// + public interface IProgressPageTransition : IPageTransition + { + /// + /// Updates the transition to reflect the given progress. + /// + /// The normalized progress value from 0.0 (start) to 1.0 (complete). + /// The visual being transitioned away from. May be null. + /// The visual being transitioned to. May be null. + /// Whether the transition direction is forward (next) or backward (previous). + /// The size of a page along the transition axis. + /// The currently visible realized pages, if more than one page is visible. + void Update( + double progress, + Visual? from, + Visual? to, + bool forward, + double pageLength, + IReadOnlyList visibleItems); + + /// + /// Resets any visual state applied to the given visual by this transition. + /// + /// The visual to reset. + void Reset(Visual visual); + } +} diff --git a/src/Avalonia.Base/Animation/PageSlide.cs b/src/Avalonia.Base/Animation/PageSlide.cs index 402f57885f..001c64f648 100644 --- a/src/Avalonia.Base/Animation/PageSlide.cs +++ b/src/Avalonia.Base/Animation/PageSlide.cs @@ -12,7 +12,7 @@ namespace Avalonia.Animation /// /// Transitions between two pages by sliding them horizontally or vertically. /// - public class PageSlide : IPageTransition + public class PageSlide : IPageTransition, IProgressPageTransition { /// /// The axis on which the PageSlide should occur @@ -50,17 +50,25 @@ namespace Avalonia.Animation /// Gets the orientation of the animation. /// public SlideAxis Orientation { get; set; } - + /// /// Gets or sets element entrance easing. /// public Easing SlideInEasing { get; set; } = new LinearEasing(); - + /// /// Gets or sets element exit easing. /// public Easing SlideOutEasing { get; set; } = new LinearEasing(); + /// + /// Gets or sets the fill mode applied to both slide animations. + /// Defaults to , which keeps the final transform value after + /// the animation completes and prevents a one-frame flash where the outgoing element snaps + /// back to its original position before IsVisible = false takes effect. + /// + public FillMode FillMode { get; set; } = FillMode.Forward; + /// public virtual async Task Start(Visual? from, Visual? to, bool forward, CancellationToken cancellationToken) { @@ -78,6 +86,7 @@ namespace Avalonia.Animation { var animation = new Animation { + FillMode = FillMode, Easing = SlideOutEasing, Children = { @@ -109,6 +118,7 @@ namespace Avalonia.Animation to.IsVisible = true; var animation = new Animation { + FillMode = FillMode, Easing = SlideInEasing, Children = { @@ -137,12 +147,70 @@ namespace Avalonia.Animation await Task.WhenAll(tasks); - if (from != null && !cancellationToken.IsCancellationRequested) + if (cancellationToken.IsCancellationRequested) + return; + + if (from != null) { from.IsVisible = false; + if (FillMode != FillMode.None) + from.RenderTransform = null; } + + if (to != null && FillMode != FillMode.None) + to.RenderTransform = null; } + /// + public virtual void Update( + double progress, + Visual? from, + Visual? to, + bool forward, + double pageLength, + IReadOnlyList visibleItems) + { + if (visibleItems.Count > 0) + return; + + if (from is null && to is null) + return; + + var parent = GetVisualParent(from, to); + var distance = pageLength > 0 + ? pageLength + : (Orientation == SlideAxis.Horizontal ? parent.Bounds.Width : parent.Bounds.Height); + var offset = distance * progress; + + if (from != null) + { + if (from.RenderTransform is not TranslateTransform ft) + from.RenderTransform = ft = new TranslateTransform(); + if (Orientation == SlideAxis.Horizontal) + ft.X = forward ? -offset : offset; + else + ft.Y = forward ? -offset : offset; + } + + if (to != null) + { + to.IsVisible = true; + if (to.RenderTransform is not TranslateTransform tt) + to.RenderTransform = tt = new TranslateTransform(); + if (Orientation == SlideAxis.Horizontal) + tt.X = forward ? distance - offset : -(distance - offset); + else + tt.Y = forward ? distance - offset : -(distance - offset); + } + } + + /// + public virtual void Reset(Visual visual) + { + visual.RenderTransform = null; + } + + /// /// Gets the common visual parent of the two control. /// diff --git a/src/Avalonia.Base/Animation/PageTransitionItem.cs b/src/Avalonia.Base/Animation/PageTransitionItem.cs new file mode 100644 index 0000000000..fed0145a2a --- /dev/null +++ b/src/Avalonia.Base/Animation/PageTransitionItem.cs @@ -0,0 +1,12 @@ +using Avalonia.VisualTree; + +namespace Avalonia.Animation +{ + /// + /// Describes a single visible page within a carousel viewport. + /// + public readonly record struct PageTransitionItem( + int Index, + Visual Visual, + double ViewportCenterOffset); +} diff --git a/src/Avalonia.Base/Animation/Transitions/Rotate3DTransition.cs b/src/Avalonia.Base/Animation/Transitions/Rotate3DTransition.cs index 239f3aea08..41aa205547 100644 --- a/src/Avalonia.Base/Animation/Transitions/Rotate3DTransition.cs +++ b/src/Avalonia.Base/Animation/Transitions/Rotate3DTransition.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Generic; using System.Threading; using System.Threading.Tasks; using Avalonia.Media; @@ -6,8 +7,10 @@ using Avalonia.Styling; namespace Avalonia.Animation; -public class Rotate3DTransition: PageSlide +public class Rotate3DTransition : PageSlide { + private const double SidePeekAngle = 24.0; + private const double FarPeekAngle = 38.0; /// /// Creates a new instance of the @@ -20,7 +23,7 @@ public class Rotate3DTransition: PageSlide { Depth = depth; } - + /// /// Defines the depth of the 3D Effect. If null, depth will be calculated automatically from the width or height /// of the common parent of the visual being rotated. @@ -28,12 +31,12 @@ public class Rotate3DTransition: PageSlide public double? Depth { get; set; } /// - /// Creates a new instance of the + /// Initializes a new instance of the class. /// public Rotate3DTransition() { } /// - public override async Task Start(Visual? @from, Visual? to, bool forward, CancellationToken cancellationToken) + public override async Task Start(Visual? from, Visual? to, bool forward, CancellationToken cancellationToken) { if (cancellationToken.IsCancellationRequested) { @@ -49,11 +52,12 @@ public class Rotate3DTransition: PageSlide _ => throw new ArgumentOutOfRangeException() }; - var depthSetter = new Setter {Property = Rotate3DTransform.DepthProperty, Value = Depth ?? center}; - var centerZSetter = new Setter {Property = Rotate3DTransform.CenterZProperty, Value = -center / 2}; + var depthSetter = new Setter { Property = Rotate3DTransform.DepthProperty, Value = Depth ?? center }; + var centerZSetter = new Setter { Property = Rotate3DTransform.CenterZProperty, Value = -center / 2 }; - KeyFrame CreateKeyFrame(double cue, double rotation, int zIndex, bool isVisible = true) => - new() { + KeyFrame CreateKeyFrame(double cue, double rotation, int zIndex, bool isVisible = true) => + new() + { Setters = { new Setter { Property = rotateProperty, Value = rotation }, @@ -71,7 +75,7 @@ public class Rotate3DTransition: PageSlide { Easing = SlideOutEasing, Duration = Duration, - FillMode = FillMode.Forward, + FillMode = FillMode, Children = { CreateKeyFrame(0d, 0d, 2), @@ -90,7 +94,7 @@ public class Rotate3DTransition: PageSlide { Easing = SlideInEasing, Duration = Duration, - FillMode = FillMode.Forward, + FillMode = FillMode, Children = { CreateKeyFrame(0d, 90d * (forward ? 1 : -1), 1), @@ -107,10 +111,8 @@ public class Rotate3DTransition: PageSlide if (!cancellationToken.IsCancellationRequested) { if (to != null) - { to.ZIndex = 2; - } - + if (from != null) { from.IsVisible = false; @@ -118,4 +120,139 @@ public class Rotate3DTransition: PageSlide } } } + + /// + public override void Update( + double progress, + Visual? from, + Visual? to, + bool forward, + double pageLength, + IReadOnlyList visibleItems) + { + if (visibleItems.Count > 0) + { + UpdateVisibleItems(progress, from, to, pageLength, visibleItems); + return; + } + + if (from is null && to is null) + return; + + var parent = GetVisualParent(from, to); + var center = pageLength > 0 + ? pageLength + : (Orientation == SlideAxis.Vertical ? parent.Bounds.Height : parent.Bounds.Width); + var depth = Depth ?? center; + var sign = forward ? 1.0 : -1.0; + + if (from != null) + { + if (from.RenderTransform is not Rotate3DTransform ft) + from.RenderTransform = ft = new Rotate3DTransform(); + ft.Depth = depth; + ft.CenterZ = -center / 2; + from.ZIndex = progress < 0.5 ? 2 : 1; + if (Orientation == SlideAxis.Horizontal) + ft.AngleY = -sign * 90.0 * progress; + else + ft.AngleX = -sign * 90.0 * progress; + } + + if (to != null) + { + to.IsVisible = true; + if (to.RenderTransform is not Rotate3DTransform tt) + to.RenderTransform = tt = new Rotate3DTransform(); + tt.Depth = depth; + tt.CenterZ = -center / 2; + to.ZIndex = progress < 0.5 ? 1 : 2; + if (Orientation == SlideAxis.Horizontal) + tt.AngleY = sign * 90.0 * (1.0 - progress); + else + tt.AngleX = sign * 90.0 * (1.0 - progress); + } + } + + private void UpdateVisibleItems( + double progress, + Visual? from, + Visual? to, + double pageLength, + IReadOnlyList visibleItems) + { + var anchor = from ?? to ?? visibleItems[0].Visual; + if (anchor.VisualParent is not Visual parent) + return; + + var center = pageLength > 0 + ? pageLength + : (Orientation == SlideAxis.Vertical ? parent.Bounds.Height : parent.Bounds.Width); + var depth = Depth ?? center; + var angleStrength = Math.Sin(Math.Clamp(progress, 0.0, 1.0) * Math.PI); + + foreach (var item in visibleItems) + { + var visual = item.Visual; + visual.IsVisible = true; + visual.ZIndex = GetZIndex(item.ViewportCenterOffset); + + if (visual.RenderTransform is not Rotate3DTransform transform) + visual.RenderTransform = transform = new Rotate3DTransform(); + + transform.Depth = depth; + transform.CenterZ = -center / 2; + + var angle = GetAngleForOffset(item.ViewportCenterOffset) * angleStrength; + if (Orientation == SlideAxis.Horizontal) + { + transform.AngleY = angle; + transform.AngleX = 0; + } + else + { + transform.AngleX = angle; + transform.AngleY = 0; + } + } + } + + private static double GetAngleForOffset(double offsetFromCenter) + { + var sign = Math.Sign(offsetFromCenter); + if (sign == 0) + return 0; + + var distance = Math.Abs(offsetFromCenter); + if (distance <= 1.0) + return sign * Lerp(0.0, SidePeekAngle, distance); + + if (distance <= 2.0) + return sign * Lerp(SidePeekAngle, FarPeekAngle, distance - 1.0); + + return sign * FarPeekAngle; + } + + private static int GetZIndex(double offsetFromCenter) + { + var distance = Math.Abs(offsetFromCenter); + + if (distance < 0.5) + return 3; + if (distance < 1.5) + return 2; + return 1; + } + + private static double Lerp(double from, double to, double t) + { + return from + ((to - from) * Math.Clamp(t, 0.0, 1.0)); + } + + /// + public override void Reset(Visual visual) + { + visual.RenderTransform = null; + visual.ZIndex = 0; + } } diff --git a/src/Avalonia.Base/Avalonia.Base.csproj b/src/Avalonia.Base/Avalonia.Base.csproj index 3b2e2d77e4..99524857a7 100644 --- a/src/Avalonia.Base/Avalonia.Base.csproj +++ b/src/Avalonia.Base/Avalonia.Base.csproj @@ -11,17 +11,16 @@ - - - - - + + + + diff --git a/src/Avalonia.Base/AvaloniaObject.cs b/src/Avalonia.Base/AvaloniaObject.cs index 3303ed276e..3ad488b615 100644 --- a/src/Avalonia.Base/AvaloniaObject.cs +++ b/src/Avalonia.Base/AvaloniaObject.cs @@ -34,7 +34,6 @@ namespace Avalonia /// public AvaloniaObject() { - VerifyAccess(); _values = new ValueStore(this); } @@ -109,16 +108,22 @@ namespace Avalonia /// internal string DebugDisplay => GetDebugDisplay(true); + /// + /// Returns the that this + /// is associated with. + /// + public Dispatcher Dispatcher { get; } = Dispatcher.CurrentDispatcher; + /// /// Returns a value indicating whether the current thread is the UI thread. /// /// true if the current thread is the UI thread; otherwise false. - public bool CheckAccess() => Dispatcher.UIThread.CheckAccess(); - + public bool CheckAccess() => Dispatcher.CheckAccess(); + /// /// Checks that the current thread is the UI thread and throws if not. /// - public void VerifyAccess() => Dispatcher.UIThread.VerifyAccess(); + public void VerifyAccess() => Dispatcher.VerifyAccess(); /// /// Clears a 's local value. diff --git a/src/Avalonia.Base/Compatibility/CollectionCompatibilityExtensions.cs b/src/Avalonia.Base/Compatibility/CollectionCompatibilityExtensions.cs deleted file mode 100644 index e22288a74d..0000000000 --- a/src/Avalonia.Base/Compatibility/CollectionCompatibilityExtensions.cs +++ /dev/null @@ -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( - this Dictionary 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(this Dictionary o, TKey key, TValue value) - where TKey : notnull - { - if (!o.ContainsKey(key)) - { - o.Add(key, value); - return true; - } - - return false; - } -} -#endif diff --git a/src/Avalonia.Base/Compatibility/NativeLibrary.cs b/src/Avalonia.Base/Compatibility/NativeLibrary.cs deleted file mode 100644 index 7627c095bc..0000000000 --- a/src/Avalonia.Base/Compatibility/NativeLibrary.cs +++ /dev/null @@ -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? DlOpen; - private static Func? DlSym; - private static Func? 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 - } -} diff --git a/src/Avalonia.Base/Compatibility/OperatingSystem.cs b/src/Avalonia.Base/Compatibility/OperatingSystem.cs deleted file mode 100644 index ad5fe0246a..0000000000 --- a/src/Avalonia.Base/Compatibility/OperatingSystem.cs +++ /dev/null @@ -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 - } -} diff --git a/src/Avalonia.Base/Compatibility/StringSyntaxAttribute.cs b/src/Avalonia.Base/Compatibility/StringSyntaxAttribute.cs deleted file mode 100644 index 2b3585fbe4..0000000000 --- a/src/Avalonia.Base/Compatibility/StringSyntaxAttribute.cs +++ /dev/null @@ -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 - /// Specifies the syntax used in a string. - [AttributeUsage(AttributeTargets.Parameter | AttributeTargets.Field | AttributeTargets.Property, AllowMultiple = false, Inherited = false)] - internal sealed class StringSyntaxAttribute : Attribute - { - /// Initializes the with the identifier of the syntax used. - /// The syntax identifier. - public StringSyntaxAttribute(string syntax) - { - Syntax = syntax; - Arguments = Array.Empty(); - } - - /// Initializes the with the identifier of the syntax used. - /// The syntax identifier. - /// Optional arguments associated with the specific syntax employed. - public StringSyntaxAttribute(string syntax, params object?[] arguments) - { - Syntax = syntax; - Arguments = arguments; - } - - /// Gets the identifier of the syntax used. - public string Syntax { get; } - - /// Optional arguments associated with the specific syntax employed. - public object?[] Arguments { get; } - - /// The syntax identifier for strings containing XML. - public const string Xml = nameof(Xml); - } -#endif -} diff --git a/src/Avalonia.Base/Data/CompiledBinding.cs b/src/Avalonia.Base/Data/CompiledBinding.cs index e243246b4f..ca8ecc62ff 100644 --- a/src/Avalonia.Base/Data/CompiledBinding.cs +++ b/src/Avalonia.Base/Data/CompiledBinding.cs @@ -28,7 +28,7 @@ public class CompiledBinding : BindingBase /// /// The binding path. public CompiledBinding(CompiledBindingPath path) => Path = path; - + /// /// Creates a from a lambda expression. /// @@ -81,12 +81,10 @@ public class CompiledBinding : BindingBase /// Indexers: x => x.Items[0] /// Type casts: x => ((DerivedType)x).Property /// Logical NOT: x => !x.BoolProperty - /// Stream bindings: x => x.TaskProperty (Task/Observable) /// AvaloniaProperty access: x => x[MyProperty] /// /// - [RequiresDynamicCode(TrimmingMessages.ExpressionNodeRequiresDynamicCodeMessage)] - [RequiresUnreferencedCode(TrimmingMessages.ExpressionNodeRequiresUnreferencedCodeMessage)] + [UnconditionalSuppressMessage("Trimming", "IL2026", Justification = "Expression statically preserves members used in binding expressions.")] public static CompiledBinding Create( Expression> expression, object? source = null, diff --git a/src/Avalonia.Base/Data/CompiledBindingPath.cs b/src/Avalonia.Base/Data/CompiledBindingPath.cs index 886d89df43..2b8b914122 100644 --- a/src/Avalonia.Base/Data/CompiledBindingPath.cs +++ b/src/Avalonia.Base/Data/CompiledBindingPath.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; using System.Reflection; using Avalonia.Controls; using Avalonia.Data.Core; @@ -155,12 +156,26 @@ namespace Avalonia.Data return this; } + [RequiresUnreferencedCode(TrimmingMessages.StreamPluginRequiresUnreferencedCodeMessage)] + public CompiledBindingPathBuilder StreamTask() + { + _elements.Add(new TaskStreamPathElement()); + return this; + } + public CompiledBindingPathBuilder StreamObservable() { _elements.Add(new ObservableStreamPathElement()); return this; } + [RequiresUnreferencedCode(TrimmingMessages.StreamPluginRequiresUnreferencedCodeMessage)] + public CompiledBindingPathBuilder StreamObservable() + { + _elements.Add(new ObservableStreamPathElement()); + return this; + } + public CompiledBindingPathBuilder Self() { _elements.Add(new SelfPathElement()); @@ -197,6 +212,12 @@ namespace Avalonia.Data return this; } + public CompiledBindingPathBuilder TypeCast(Type targetType) + { + _elements.Add(new TypeCastPathElement(targetType)); + return this; + } + public CompiledBindingPathBuilder TemplatedParent() { _elements.Add(new TemplatedParentPathElement()); @@ -299,6 +320,14 @@ namespace Avalonia.Data public IStreamPlugin CreatePlugin() => new TaskStreamPlugin(); } + [RequiresUnreferencedCode(TrimmingMessages.StreamPluginRequiresUnreferencedCodeMessage)] + internal class TaskStreamPathElement : IStronglyTypedStreamElement + { + public static readonly TaskStreamPathElement Instance = new TaskStreamPathElement(); + + public IStreamPlugin CreatePlugin() => new TaskStreamPlugin(); + } + internal class ObservableStreamPathElement : IStronglyTypedStreamElement { public static readonly ObservableStreamPathElement Instance = new ObservableStreamPathElement(); @@ -306,6 +335,14 @@ namespace Avalonia.Data public IStreamPlugin CreatePlugin() => new ObservableStreamPlugin(); } + [RequiresUnreferencedCode(TrimmingMessages.StreamPluginRequiresUnreferencedCodeMessage)] + internal class ObservableStreamPathElement : IStronglyTypedStreamElement + { + public static readonly ObservableStreamPathElement Instance = new ObservableStreamPathElement(); + + public IStreamPlugin CreatePlugin() => new ObservableStreamPlugin(); + } + internal class SelfPathElement : ICompiledBindingPathElement, IControlSourceBindingPathElement { public static readonly SelfPathElement Instance = new SelfPathElement(); @@ -387,7 +424,28 @@ namespace Avalonia.Data public Type Type => typeof(T); - public Func Cast => TryCast; + public Func Cast { get; } = TryCast; + + public override string ToString() + => $"({Type.FullName})"; + } + + internal class TypeCastPathElement : ITypeCastElement + { + public TypeCastPathElement(Type type) + { + Type = type; + Cast = obj => + { + if (obj is { } result && type.IsInstanceOfType(result)) + return result; + return null; + }; + } + + public Type Type { get; } + + public Func Cast { get; } public override string ToString() => $"({Type.FullName})"; diff --git a/src/Avalonia.Base/Data/Core/BindingExpression.cs b/src/Avalonia.Base/Data/Core/BindingExpression.cs index 00a90f87fe..81a21955c4 100644 --- a/src/Avalonia.Base/Data/Core/BindingExpression.cs +++ b/src/Avalonia.Base/Data/Core/BindingExpression.cs @@ -460,12 +460,9 @@ internal class BindingExpression : UntypedBindingExpressionBase, IDescription, I StopDelayTimer(); if (TryGetTarget(out var target) && - TargetProperty is not null && - target.GetValue(TargetProperty) is var value && - LeafNode is { } leafNode && - !TypeUtilities.IdentityEquals(value, leafNode.Value, TargetType)) + TargetProperty is not null) { - WriteValueToSource(value); + WriteValueToSource(target.GetValue(TargetProperty)); } } diff --git a/src/Avalonia.Base/Data/Core/ExpressionNodes/Reflection/DynamicPluginStreamNode.cs b/src/Avalonia.Base/Data/Core/ExpressionNodes/Reflection/DynamicPluginStreamNode.cs index dd8c0e1a63..198819f1ac 100644 --- a/src/Avalonia.Base/Data/Core/ExpressionNodes/Reflection/DynamicPluginStreamNode.cs +++ b/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; diff --git a/src/Avalonia.Base/Data/Core/ExpressionNodes/Reflection/ExpressionTreeIndexerNode.cs b/src/Avalonia.Base/Data/Core/ExpressionNodes/Reflection/ExpressionTreeIndexerNode.cs index dfb83fb10d..ef8aa34752 100644 --- a/src/Avalonia.Base/Data/Core/ExpressionNodes/Reflection/ExpressionTreeIndexerNode.cs +++ b/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); diff --git a/src/Avalonia.Base/Data/Core/Parsers/BindingExpressionVisitor.cs b/src/Avalonia.Base/Data/Core/Parsers/BindingExpressionVisitor.cs index c354bcc15a..8525dd8493 100644 --- a/src/Avalonia.Base/Data/Core/Parsers/BindingExpressionVisitor.cs +++ b/src/Avalonia.Base/Data/Core/Parsers/BindingExpressionVisitor.cs @@ -1,5 +1,4 @@ using System; -using System.Collections.Generic; using System.Collections.Specialized; using System.ComponentModel; using System.Diagnostics.CodeAnalysis; @@ -8,9 +7,7 @@ using System.Linq.Expressions; using System.Reflection; using System.Threading.Tasks; using Avalonia.Data.Core.ExpressionNodes; -using Avalonia.Data.Core.ExpressionNodes.Reflection; using Avalonia.Data.Core.Plugins; -using Avalonia.Reactive; using Avalonia.Utilities; namespace Avalonia.Data.Core.Parsers; @@ -25,7 +22,6 @@ namespace Avalonia.Data.Core.Parsers; /// can then be converted into instances. It supports property access, /// indexers, AvaloniaProperty access, stream bindings, type casts, and logical operators. /// -[RequiresDynamicCode(TrimmingMessages.ExpressionNodeRequiresDynamicCodeMessage)] [RequiresUnreferencedCode(TrimmingMessages.ExpressionNodeRequiresUnreferencedCodeMessage)] internal class BindingExpressionVisitor(LambdaExpression expression) : ExpressionVisitor { @@ -149,17 +145,11 @@ internal class BindingExpressionVisitor(LambdaExpression expression) : Expr instanceType.GetGenericTypeDefinition() == typeof(Task<>) && genericArg.IsAssignableFrom(instanceType.GetGenericArguments()[0]))) { - var builderMethod = typeof(CompiledBindingPathBuilder) - .GetMethod(nameof(CompiledBindingPathBuilder.StreamTask))! - .MakeGenericMethod(genericArg); - return Add(instance, node, x => builderMethod.Invoke(x, null)); + return Add(instance, node, x => x.StreamTask()); } - else if (typeof(IObservable<>).MakeGenericType(genericArg).IsAssignableFrom(instance?.Type)) + else if (instanceType is not null && ObservableStreamPlugin.MatchesType(instanceType)) { - var builderMethod = typeof(CompiledBindingPathBuilder) - .GetMethod(nameof(CompiledBindingPathBuilder.StreamObservable))! - .MakeGenericMethod(genericArg); - return Add(instance, node, x => builderMethod.Invoke(x, null)); + return Add(instance, node, x => x.StreamObservable()); } } else if (method == BindingExpressionVisitorMembers.CreateDelegateMethod) @@ -194,18 +184,12 @@ internal class BindingExpressionVisitor(LambdaExpression expression) : Expr if (!node.Type.IsValueType && !node.Operand.Type.IsValueType && (node.Type.IsAssignableFrom(node.Operand.Type) || node.Operand.Type.IsAssignableFrom(node.Type))) { - var castMethod = typeof(CompiledBindingPathBuilder) - .GetMethod(nameof(CompiledBindingPathBuilder.TypeCast))! - .MakeGenericMethod(node.Type); - return Add(node.Operand, node, x => castMethod.Invoke(x, null)); + return Add(node.Operand, node, x => x.TypeCast(node.Type)); } } else if (node.NodeType == ExpressionType.TypeAs) { - var castMethod = typeof(CompiledBindingPathBuilder) - .GetMethod(nameof(CompiledBindingPathBuilder.TypeCast))! - .MakeGenericMethod(node.Type); - return Add(node.Operand, node, x => castMethod.Invoke(x, null)); + return Add(node.Operand, node, x => x.TypeCast(node.Type)); } throw new ExpressionParseException(0, $"Invalid expression type in binding expression: {node.NodeType}."); diff --git a/src/Avalonia.Base/Data/Core/Parsers/ExpressionNodeFactory.cs b/src/Avalonia.Base/Data/Core/Parsers/ExpressionNodeFactory.cs index f7eb2d537d..301e8c0796 100644 --- a/src/Avalonia.Base/Data/Core/Parsers/ExpressionNodeFactory.cs +++ b/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? CreateFromAst( List astNodes, Func? typeResolver, diff --git a/src/Avalonia.Base/Data/Core/Plugins/BindingPlugins.cs b/src/Avalonia.Base/Data/Core/Plugins/BindingPlugins.cs index 50e137eac1..516dbecc26 100644 --- a/src/Avalonia.Base/Data/Core/Plugins/BindingPlugins.cs +++ b/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()); } diff --git a/src/Avalonia.Base/Data/Core/Plugins/ObservableStreamPlugin.cs b/src/Avalonia.Base/Data/Core/Plugins/ObservableStreamPlugin.cs index 523c0bcf39..b747542ee5 100644 --- a/src/Avalonia.Base/Data/Core/Plugins/ObservableStreamPlugin.cs +++ b/src/Avalonia.Base/Data/Core/Plugins/ObservableStreamPlugin.cs @@ -31,7 +31,18 @@ namespace Avalonia.Data.Core.Plugins { reference.TryGetTarget(out var target); - return target != null && target.GetType().GetInterfaces().Any(x => + return target != null && MatchesType(target.GetType()); + } + + public static bool MatchesType(Type type) + { + var interfaces = type.GetInterfaces().AsEnumerable(); + if (type.IsInterface) + { + interfaces = interfaces.Concat([type]); + } + + return interfaces.Any(x => x.IsGenericType && x.GetGenericTypeDefinition() == typeof(IObservable<>)); } diff --git a/src/Avalonia.Base/Data/Core/Plugins/ReflectionMethodAccessorPlugin.cs b/src/Avalonia.Base/Data/Core/Plugins/ReflectionMethodAccessorPlugin.cs index f9a4587ca6..d2e6f23e29 100644 --- a/src/Avalonia.Base/Data/Core/Plugins/ReflectionMethodAccessorPlugin.cs +++ b/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 reference, MethodInfo method) diff --git a/src/Avalonia.Base/Data/Core/Plugins/TaskStreamPlugin.cs b/src/Avalonia.Base/Data/Core/Plugins/TaskStreamPlugin.cs index 56a7fcefe4..5d395b8b5d 100644 --- a/src/Avalonia.Base/Data/Core/Plugins/TaskStreamPlugin.cs +++ b/src/Avalonia.Base/Data/Core/Plugins/TaskStreamPlugin.cs @@ -61,10 +61,9 @@ namespace Avalonia.Data.Core.Plugins return Observable.Empty(); } - [RequiresUnreferencedCode(TrimmingMessages.StreamPluginRequiresUnreferencedCodeMessage)] private static IObservable HandleCompleted(Task task) { - var resultProperty = task.GetType().GetRuntimeProperty("Result"); + var resultProperty = GetTaskResult(task); if (resultProperty != null) { @@ -80,6 +79,14 @@ namespace Avalonia.Data.Core.Plugins } return Observable.Empty(); + + [DynamicDependency("Result", typeof(Task<>))] + [UnconditionalSuppressMessage("Trimming", "IL2070")] + [UnconditionalSuppressMessage("Trimming", "IL2075")] + PropertyInfo? GetTaskResult(Task obj) + { + return obj.GetType().GetProperty("Result"); + } } } } diff --git a/src/Avalonia.Base/Input/AccessKeyHandler.cs b/src/Avalonia.Base/Input/AccessKeyHandler.cs index 758e28e07f..97d08b5620 100644 --- a/src/Avalonia.Base/Input/AccessKeyHandler.cs +++ b/src/Avalonia.Base/Input/AccessKeyHandler.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.Linq; +using Avalonia.Controls.Primitives; using Avalonia.Interactivity; using Avalonia.LogicalTree; using Avalonia.VisualTree; @@ -30,6 +31,12 @@ namespace Avalonia.Input RoutingStrategies.Bubble, typeof(AccessKeyHandler)); + /// + /// Defines the ShowAccessKey attached property. + /// + public static readonly AttachedProperty ShowAccessKeyProperty = + AvaloniaProperty.RegisterAttached("ShowAccessKey", inherits: true); + /// /// The registered access keys. /// @@ -40,7 +47,7 @@ namespace Avalonia.Input /// /// The window to which the handler belongs. /// - private IInputRoot? _owner; + private InputElement? _owner; /// /// Whether access keys are currently being shown; @@ -96,7 +103,7 @@ namespace Avalonia.Input /// /// This method can only be called once, typically by the owner itself on creation. /// - public void SetOwner(IInputRoot owner) + public void SetOwner(InputElement owner) { if (_owner != null) { @@ -113,7 +120,7 @@ namespace Avalonia.Input OnSetOwner(owner); } - protected virtual void OnSetOwner(IInputRoot owner) + protected virtual void OnSetOwner(InputElement owner) { } @@ -159,6 +166,9 @@ namespace Avalonia.Input } } + static void SetShowAccessKeys(AvaloniaObject target, bool value) => + target.SetValue(ShowAccessKeyProperty, value); + /// /// Called when a key is pressed in the owner window. /// @@ -188,7 +198,7 @@ namespace Avalonia.Input // When Alt is pressed without a main menu, or with a closed main menu, show // access key markers in the window (i.e. "_File"). - _owner!.ShowAccessKeys = _showingAccessKeys = isFocusWithinOwner; + SetShowAccessKeys(_owner!, _showingAccessKeys = isFocusWithinOwner); } else { @@ -265,7 +275,7 @@ namespace Avalonia.Input { if (_showingAccessKeys) { - _owner!.ShowAccessKeys = false; + SetShowAccessKeys(_owner!, false); } } @@ -275,12 +285,12 @@ namespace Avalonia.Input private void CloseMenu() { MainMenu!.Close(); - _owner!.ShowAccessKeys = _showingAccessKeys = false; + SetShowAccessKeys(_owner!, _showingAccessKeys = false); } private void MainMenuClosed(object? sender, EventArgs e) { - _owner!.ShowAccessKeys = false; + SetShowAccessKeys(_owner!, false); } /// @@ -444,7 +454,7 @@ namespace Avalonia.Input /// /// The owner to check. /// If focused element is decendant of owner true, otherwise false. - private static bool IsFocusWithinOwner(IInputRoot owner) + private static bool IsFocusWithinOwner(IInputElement owner) { var focusedElement = KeyboardDevice.Instance?.FocusedElement; if (focusedElement is not InputElement inputElement) diff --git a/src/Avalonia.Controls/ContextRequestedEventArgs.cs b/src/Avalonia.Base/Input/ContextRequestedEventArgs.cs similarity index 86% rename from src/Avalonia.Controls/ContextRequestedEventArgs.cs rename to src/Avalonia.Base/Input/ContextRequestedEventArgs.cs index fa0d5f9855..4d8cf333d1 100644 --- a/src/Avalonia.Controls/ContextRequestedEventArgs.cs +++ b/src/Avalonia.Base/Input/ContextRequestedEventArgs.cs @@ -1,7 +1,6 @@ -using Avalonia.Input; -using Avalonia.Interactivity; +using Avalonia.Interactivity; -namespace Avalonia.Controls +namespace Avalonia.Input { /// /// Provides event data for the ContextRequested event. @@ -14,7 +13,7 @@ namespace Avalonia.Controls /// Initializes a new instance of the ContextRequestedEventArgs class. /// public ContextRequestedEventArgs() - : base(Control.ContextRequestedEvent) + : base(InputElement.ContextRequestedEvent) { } @@ -34,10 +33,10 @@ namespace Avalonia.Controls } /// - /// Gets the x- and y-coordinates of the pointer position, optionally evaluated against a coordinate origin of a supplied . + /// Gets the x- and y-coordinates of the pointer position, optionally evaluated against a coordinate origin of a supplied . /// /// - /// Any -derived object that is connected to the same object tree. + /// Any -derived object that is connected to the same object tree. /// To specify the object relative to the overall coordinate system, use a relativeTo value of null. /// /// @@ -48,7 +47,7 @@ namespace Avalonia.Controls /// /// true if the context request was initiated by a pointer device; otherwise, false. /// - public bool TryGetPosition(Control? relativeTo, out Point point) + public bool TryGetPosition(InputElement? relativeTo, out Point point) { if (_pointerEventArgs is null) { diff --git a/src/Avalonia.Base/Input/Cursor.cs b/src/Avalonia.Base/Input/Cursor.cs index 2de2f12aff..21726898b5 100644 --- a/src/Avalonia.Base/Input/Cursor.cs +++ b/src/Avalonia.Base/Input/Cursor.cs @@ -56,7 +56,7 @@ namespace Avalonia.Input } public Cursor(Bitmap cursor, PixelPoint hotSpot) - : this(GetCursorFactory().CreateCursor(cursor.PlatformImpl.Item, hotSpot), "BitmapCursor") + : this(GetCursorFactory().CreateCursor(cursor, hotSpot), "BitmapCursor") { } diff --git a/src/Avalonia.Base/Input/DataFormat.cs b/src/Avalonia.Base/Input/DataFormat.cs index 7e35bab411..14d1d4a30b 100644 --- a/src/Avalonia.Base/Input/DataFormat.cs +++ b/src/Avalonia.Base/Input/DataFormat.cs @@ -213,19 +213,7 @@ public abstract class DataFormat : IEquatable 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 == '-'; } /// diff --git a/src/Avalonia.Base/Input/DragDropDevice.cs b/src/Avalonia.Base/Input/DragDropDevice.cs index 8f3b542e8d..21321510e6 100644 --- a/src/Avalonia.Base/Input/DragDropDevice.cs +++ b/src/Avalonia.Base/Input/DragDropDevice.cs @@ -16,7 +16,7 @@ namespace Avalonia.Input private static Interactive? GetTarget(IInputRoot root, Point local) { - var hit = root.InputHitTest(local) as Visual; + var hit = root.RootElement?.InputHitTest(local) as Visual; var target = hit?.GetSelfAndVisualAncestors()?.OfType()?.FirstOrDefault(); if (target != null && DragDrop.GetAllowDrop(target)) return target; @@ -35,7 +35,7 @@ namespace Avalonia.Input if (target == null) return DragDropEffects.None; - var p = ((Visual)inputRoot).TranslatePoint(point, target); + var p = (inputRoot.RootElement).TranslatePoint(point, target); if (!p.HasValue) return DragDropEffects.None; diff --git a/src/Avalonia.Base/Input/DragEventArgs.cs b/src/Avalonia.Base/Input/DragEventArgs.cs index e68a6138e0..d4e0cb1bce 100644 --- a/src/Avalonia.Base/Input/DragEventArgs.cs +++ b/src/Avalonia.Base/Input/DragEventArgs.cs @@ -1,6 +1,5 @@ using System; using Avalonia.Interactivity; -using Avalonia.Metadata; namespace Avalonia.Input { @@ -25,9 +24,8 @@ namespace Avalonia.Input return _target.TranslatePoint(_targetLocation, relativeTo) ?? new Point(0, 0); } - [Unstable("This constructor might be removed in 12.0. For unit testing, consider using DragDrop.DoDragDrop or IHeadlessWindow.DragDrop.")] public DragEventArgs( - RoutedEvent routedEvent, + RoutedEvent? routedEvent, IDataTransfer dataTransfer, Interactive target, Point targetLocation, diff --git a/src/Avalonia.Base/Input/FindNextElementOptions.cs b/src/Avalonia.Base/Input/FindNextElementOptions.cs index e6062daf9b..ee9c2e6fef 100644 --- a/src/Avalonia.Base/Input/FindNextElementOptions.cs +++ b/src/Avalonia.Base/Input/FindNextElementOptions.cs @@ -1,17 +1,82 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; - -namespace Avalonia.Input +namespace Avalonia.Input { + /// + /// Provides options to customize the behavior when identifying the next element to focus + /// during a navigation operation. + /// public sealed class FindNextElementOptions { + /// + /// Gets or sets the element that will be treated as the starting point of the search + /// for the next focusable element. This does not need to be the element that is + /// currently focused. If null, is used. + /// + public IInputElement? FocusedElement { get; init; } + + /// + /// Gets or sets the root within which the search for the next + /// focusable element will be conducted. + /// + /// + /// + /// 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. + /// + /// + /// This option is only used with , , + /// , and . It is ignored for other + /// directions. + /// + /// public InputElement? SearchRoot { get; init; } + + /// + /// Gets or sets the rectangular region within the visual hierarchy that will be excluded + /// from consideration during focus navigation. + /// + /// + /// This option is only used with , , + /// , and . It is ignored for other + /// directions. + /// public Rect ExclusionRect { get; init; } + + /// + /// 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. + /// + /// + /// This option is only used with , , + /// , and . It is ignored for other + /// directions. + /// public Rect? FocusHintRectangle { get; init; } + + /// + /// 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. + /// + /// + /// This option is only used with , , + /// , and . It is ignored for other + /// directions. + /// public XYFocusNavigationStrategy? NavigationStrategyOverride { get; init; } + + /// + /// Specifies whether occlusivity (overlapping of elements or obstructions) + /// should be ignored during focus navigation. When set to true, + /// the navigation logic disregards obstructions that may block a potential + /// focus target, allowing elements behind such obstructions to be considered. + /// + /// + /// This option is only used with , , + /// , and . It is ignored for other + /// directions. + /// public bool IgnoreOcclusivity { get; init; } } } diff --git a/src/Avalonia.Base/Input/FocusChangedEventArgs.cs b/src/Avalonia.Base/Input/FocusChangedEventArgs.cs new file mode 100644 index 0000000000..ecca7750ed --- /dev/null +++ b/src/Avalonia.Base/Input/FocusChangedEventArgs.cs @@ -0,0 +1,39 @@ +using Avalonia.Interactivity; + +namespace Avalonia.Input +{ + /// + /// Represents the arguments of and . + /// + public class FocusChangedEventArgs : RoutedEventArgs, IKeyModifiersEventArgs + { + /// + /// Initializes a new instance of . + /// + /// The routed event associated with these event args. + public FocusChangedEventArgs(RoutedEvent? routedEvent) + : base(routedEvent) + { + } + + /// + /// Gets or sets the element that focus has moved to. + /// + public IInputElement? NewFocusedElement { get; init; } + + /// + /// Gets or sets the element that previously had focus. + /// + public IInputElement? OldFocusedElement { get; init; } + + /// + /// Gets or sets a value indicating how the change in focus occurred. + /// + public NavigationMethod NavigationMethod { get; init; } + + /// + /// Gets or sets any key modifiers active at the time of focus. + /// + public KeyModifiers KeyModifiers { get; init; } + } +} diff --git a/src/Avalonia.Base/Input/FocusChangingEventArgs.cs b/src/Avalonia.Base/Input/FocusChangingEventArgs.cs index 372ddf38b6..ed237265a6 100644 --- a/src/Avalonia.Base/Input/FocusChangingEventArgs.cs +++ b/src/Avalonia.Base/Input/FocusChangingEventArgs.cs @@ -12,7 +12,7 @@ namespace Avalonia.Input /// /// Provides data for focus changing. /// - internal FocusChangingEventArgs(RoutedEvent routedEvent) : base(routedEvent) + public FocusChangingEventArgs(RoutedEvent? routedEvent) : base(routedEvent) { } diff --git a/src/Avalonia.Base/Input/FocusManager.cs b/src/Avalonia.Base/Input/FocusManager.cs index ed741a36ae..a273ff6d89 100644 --- a/src/Avalonia.Base/Input/FocusManager.cs +++ b/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 /// /// Manages focus for the application. /// - [PrivateApi] public class FocusManager : IFocusManager { /// @@ -42,53 +40,51 @@ namespace Avalonia.Input RoutingStrategies.Tunnel); } + [PrivateApi] public FocusManager() { - _contentRoot = null; } - public FocusManager(IInputElement contentRoot) + /// + /// Gets or sets the content root for the focus management system. + /// + [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; - /// - /// Gets the currently focused . - /// + /// public IInputElement? GetFocusedElement() => Current; - /// - /// Focuses a control. - /// - /// The control to focus. - /// The method by which focus was changed. - /// Any key modifiers active at the time of focus. + /// 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 && @@ -105,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 && @@ -120,10 +111,11 @@ namespace Avalonia.Input scope.ClearValue(FocusedElementProperty); } - if (Current == removedElement) + if (Current == removedElement) Focus(null); } + [PrivateApi] public IInputElement? GetFocusedElement(IFocusScope scope) { return (scope as StyledElement)?.GetValue(FocusedElementProperty); @@ -133,6 +125,7 @@ namespace Avalonia.Input /// Notifies the focus manager of a change in focus scope. /// /// The new focus scope. + [PrivateApi] public void SetFocusScope(IFocusScope scope) { if (GetFocusedElement(scope) is { } focused) @@ -148,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; /// @@ -163,32 +158,23 @@ namespace Avalonia.Input /// internal static FocusManager? GetFocusManager(IInputElement? element) { + // Element might not be a visual, and not attached to the root. // But IFocusManager is always expected to be a FocusManager. - return (FocusManager?)((element as Visual)?.VisualRoot as IInputRoot)?.FocusManager + return (FocusManager?)(element as Visual)?.GetInputRoot()?.FocusManager // In our unit tests some elements might not have a root. Remove when we migrate to headless tests. ?? (FocusManager?)AvaloniaLocator.Current.GetService(); } - /// - /// Attempts to change focus from the element with focus to the next focusable element in the specified direction. - /// - /// The direction to traverse (in tab order). - /// true if focus moved; otherwise, false. - public bool TryMoveFocus(NavigationDirection direction) + /// + public bool TryMoveFocus(NavigationDirection direction, FindNextElementOptions? options = null) { - return FindAndSetNextFocus(direction, _xYFocusOptions); - } + ValidateDirection(direction); - /// - /// Attempts to change focus from the element with focus to the next focusable element in the specified direction, using the specified navigation options. - /// - /// The direction to traverse (in tab order). - /// The options to help identify the next element to receive focus with keyboard/controller/remote navigation. - /// true if focus moved; otherwise, false. - public bool TryMoveFocus(NavigationDirection direction, FindNextElementOptions options) - { - return FindAndSetNextFocus(direction, ValidateAndCreateFocusOptions(direction, options)); + var focusOptions = ToFocusOptions(options, true); + var result = FindAndSetNextFocus(options?.FocusedElement ?? Current, direction, focusOptions); + _reusableFocusOptions = focusOptions; + return result; } /// @@ -240,10 +226,10 @@ namespace Avalonia.Input if (scope is not Visual v) return null; - var root = v.VisualRoot as Visual; + var root = v.PresentationSource?.InputRoot.FocusRoot as Visual; while (root is IHostedVisualTreeRoot hosted && - hosted.Host?.VisualRoot is Visual parentRoot) + hosted.Host?.PresentationSource?.InputRoot.FocusRoot is {} parentRoot) { root = parentRoot; } @@ -289,10 +275,7 @@ namespace Avalonia.Input return true; } - /// - /// Retrieves the first element that can receive focus. - /// - /// The first focusable element. + /// public IInputElement? FindFirstFocusableElement() { var root = (_contentRoot as Visual)?.GetSelfAndVisualDescendants().FirstOrDefault(x => x is IInputElement) as IInputElement; @@ -311,10 +294,7 @@ namespace Avalonia.Input return GetFirstFocusableElement(searchScope); } - /// - /// Retrieves the last element that can receive focus. - /// - /// The last focusable element. + /// public IInputElement? FindLastFocusableElement() { var root = (_contentRoot as Visual)?.GetSelfAndVisualDescendants().FirstOrDefault(x => x is IInputElement) as IInputElement; @@ -333,75 +313,84 @@ namespace Avalonia.Input return GetFocusManager(searchScope)?.GetLastFocusableElement(searchScope); } - /// - /// Retrieves the element that should receive focus based on the specified navigation direction. - /// - /// - /// - public IInputElement? FindNextElement(NavigationDirection direction) + /// + 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(options?.FocusedElement ?? Current, direction, focusOptions); + _reusableFocusOptions = focusOptions; + return result; } - /// - /// Retrieves the element that should receive focus based on the specified navigation direction (cannot be used with tab navigation). - /// - /// The direction that focus moves from element to element within the app UI. - /// The options to help identify the next element to receive focus with the provided navigation. - /// The next element to receive focus. - 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) + private IInputElement? FindNextFocus( + IInputElement? focusedElement, + NavigationDirection direction, + XYFocusOptions focusOptions, + bool updateManifolds = true) { - IInputElement? nextFocusedElement = null; + IInputElement? nextFocusedElement; - var currentlyFocusedElement = Current; - - if (direction is NavigationDirection.Previous or NavigationDirection.Next || currentlyFocusedElement == null) + if (direction is NavigationDirection.Previous or NavigationDirection.Next || focusedElement == null) { var isReverse = direction == NavigationDirection.Previous; - nextFocusedElement = ProcessTabStopInternal(isReverse, true); + nextFocusedElement = ProcessTabStopInternal(focusedElement, isReverse, true); } else { - if (currentlyFocusedElement is InputElement inputElement && + if (focusedElement is InputElement inputElement && XYFocus.GetBoundsForRanking(inputElement, focusOptions.IgnoreClipping) is { } bounds) { focusOptions.FocusedElementBounds = bounds; } nextFocusedElement = _xyFocus.GetNextFocusableElement(direction, - currentlyFocusedElement as InputElement, + focusedElement as InputElement, null, updateManifolds, focusOptions); @@ -522,14 +511,14 @@ namespace Avalonia.Input return lastFocus; } - private IInputElement? ProcessTabStopInternal(bool isReverse, bool queryOnly) + private IInputElement? ProcessTabStopInternal(IInputElement? focusedElement, bool isReverse, bool queryOnly) { IInputElement? newTabStop = null; - var defaultCandidateTabStop = GetTabStopCandidateElement(isReverse, queryOnly, out var didCycleFocusAtRootVisualScope); + var defaultCandidateTabStop = GetTabStopCandidateElement(focusedElement, isReverse, queryOnly, out var didCycleFocusAtRootVisualScope); var isTabStopOverriden = InputElement.ProcessTabStop(_contentRoot, - Current, + focusedElement, defaultCandidateTabStop, isReverse, didCycleFocusAtRootVisualScope, @@ -548,24 +537,27 @@ namespace Avalonia.Input return newTabStop; } - private IInputElement? GetTabStopCandidateElement(bool isReverse, bool queryOnly, out bool didCycleFocusAtRootVisualScope) + private IInputElement? GetTabStopCandidateElement( + IInputElement? focusedElement, + bool isReverse, + bool queryOnly, + out bool didCycleFocusAtRootVisualScope) { didCycleFocusAtRootVisualScope = false; - var currentFocus = Current; - IInputElement? newTabStop = null; - var root = this._contentRoot as IInputElement; + IInputElement? newTabStop; + var root = _contentRoot; if (root == null) return null; bool internalCycleWorkaround = false; - if (Current != null) + if (focusedElement != null) { - internalCycleWorkaround = CanProcessTabStop(isReverse); + internalCycleWorkaround = CanProcessTabStop(focusedElement, isReverse); } - if (currentFocus == null) + if (focusedElement == null) { if (!isReverse) { @@ -580,7 +572,7 @@ namespace Avalonia.Input } else if (!isReverse) { - newTabStop = GetNextTabStop(); + newTabStop = GetNextTabStop(focusedElement); if (newTabStop == null && (internalCycleWorkaround || queryOnly)) { @@ -591,7 +583,7 @@ namespace Avalonia.Input } else { - newTabStop = GetPreviousTabStop(); + newTabStop = GetPreviousTabStop(focusedElement); if (newTabStop == null && (internalCycleWorkaround || queryOnly)) { @@ -603,9 +595,9 @@ namespace Avalonia.Input return newTabStop; } - private IInputElement? GetNextTabStop(IInputElement? currentTabStop = null, bool ignoreCurrentTabStop = false) + private IInputElement? GetNextTabStop(IInputElement? currentTabStop, bool ignoreCurrentTabStop = false) { - var focused = currentTabStop ?? Current; + var focused = currentTabStop; if (focused == null || _contentRoot == null) { return null; @@ -711,9 +703,9 @@ namespace Avalonia.Input return newTabStop; } - private IInputElement? GetPreviousTabStop(IInputElement? currentTabStop = null, bool ignoreCurrentTabStop = false) + private IInputElement? GetPreviousTabStop(IInputElement? currentTabStop, bool ignoreCurrentTabStop = false) { - var focused = currentTabStop ?? Current; + var focused = currentTabStop; if (focused == null || _contentRoot == null) { return null; @@ -943,23 +935,23 @@ namespace Avalonia.Input return int.MaxValue; } - private bool CanProcessTabStop(bool isReverse) + private bool CanProcessTabStop(IInputElement? focusedElement, bool isReverse) { bool isFocusOnFirst = false; bool isFocusOnLast = false; bool canProcessTab = true; - if (IsFocusedElementInPopup()) + if (IsFocusedElementInPopup(focusedElement)) { return true; } if (isReverse) { - isFocusOnFirst = IsFocusOnFirstTabStop(); + isFocusOnFirst = IsFocusOnFirstTabStop(focusedElement); } else { - isFocusOnLast = IsFocusOnLastTabStop(); + isFocusOnLast = IsFocusOnLastTabStop(focusedElement); } if (isFocusOnFirst || isFocusOnLast) @@ -974,7 +966,7 @@ namespace Avalonia.Input if (edge != null) { var edgeParent = GetParentTabStopElement(edge); - if (edgeParent is InputElement inputElement && KeyboardNavigation.GetTabNavigation(inputElement) == KeyboardNavigationMode.Once && edgeParent == GetParentTabStopElement(Current)) + if (edgeParent is InputElement inputElement && KeyboardNavigation.GetTabNavigation(inputElement) == KeyboardNavigationMode.Once && edgeParent == GetParentTabStopElement(focusedElement)) { canProcessTab = false; } @@ -988,13 +980,13 @@ namespace Avalonia.Input { if (isFocusOnLast || isFocusOnFirst) { - if (Current is InputElement inputElement && KeyboardNavigation.GetTabNavigation(inputElement) == KeyboardNavigationMode.Cycle) + if (focusedElement is InputElement inputElement && KeyboardNavigation.GetTabNavigation(inputElement) == KeyboardNavigationMode.Cycle) { canProcessTab = true; } else { - var focusedParent = GetParentTabStopElement(Current); + var focusedParent = GetParentTabStopElement(focusedElement); while (focusedParent != null) { if (focusedParent is InputElement iE && KeyboardNavigation.GetTabNavigation(iE) == KeyboardNavigationMode.Cycle) @@ -1054,9 +1046,9 @@ namespace Avalonia.Input return null; } - private bool IsFocusOnLastTabStop() + private bool IsFocusOnLastTabStop(IInputElement? focusedElement) { - if (Current == null || _contentRoot is not Visual visual) + if (focusedElement == null || _contentRoot is not Visual visual) return false; var root = visual.VisualRoot as IInputElement; @@ -1064,12 +1056,12 @@ namespace Avalonia.Input var lastFocus = GetLastFocusableElement(root, null); - return lastFocus == Current; + return lastFocus == focusedElement; } - private bool IsFocusOnFirstTabStop() + private bool IsFocusOnFirstTabStop(IInputElement? focusedElement) { - if (Current == null || _contentRoot is not Visual visual) + if (focusedElement == null || _contentRoot is not Visual visual) return false; var root = visual.VisualRoot as IInputElement; @@ -1077,7 +1069,7 @@ namespace Avalonia.Input var firstFocus = GetFirstFocusableElement(root, null); - return firstFocus == Current; + return firstFocus == focusedElement; } private static IInputElement? GetFirstFocusableElement(IInputElement searchStart, IInputElement? firstFocus = null) @@ -1104,7 +1096,7 @@ namespace Avalonia.Input return lastFocus; } - private bool IsFocusedElementInPopup() => Current != null && GetRootOfPopupSubTree(Current) != null; + private bool IsFocusedElementInPopup(IInputElement? focusedElement) => focusedElement != null && GetRootOfPopupSubTree(focusedElement) != null; private Visual? GetRootOfPopupSubTree(IInputElement? current) { @@ -1112,7 +1104,7 @@ namespace Avalonia.Input return null; } - private bool FindAndSetNextFocus(NavigationDirection direction, XYFocusOptions xYFocusOptions) + private bool FindAndSetNextFocus(IInputElement? focusedElement, NavigationDirection direction, XYFocusOptions xYFocusOptions) { var focusChanged = false; if (xYFocusOptions.UpdateManifoldsFromFocusHintRect && xYFocusOptions.FocusHintRectangle != null) @@ -1120,7 +1112,7 @@ namespace Avalonia.Input _xyFocus.SetManifoldsFromBounds(xYFocusOptions.FocusHintRectangle ?? default); } - if (FindNextFocus(direction, xYFocusOptions, false) is { } nextFocusedElement) + if (FindNextFocus(focusedElement, direction, xYFocusOptions, false) is { } nextFocusedElement) { focusChanged = nextFocusedElement.Focus(); diff --git a/src/Avalonia.Base/Input/GestureRecognizers/GestureRecognizerCollection.cs b/src/Avalonia.Base/Input/GestureRecognizers/GestureRecognizerCollection.cs index 74e8061292..34e900c7d7 100644 --- a/src/Avalonia.Base/Input/GestureRecognizers/GestureRecognizerCollection.cs +++ b/src/Avalonia.Base/Input/GestureRecognizers/GestureRecognizerCollection.cs @@ -39,6 +39,24 @@ namespace Avalonia.Input.GestureRecognizers } } + public bool Remove(GestureRecognizer recognizer) + { + if (_recognizers == null) + return false; + + var removed = _recognizers.Remove(recognizer); + + if (removed) + { + recognizer.Target = null; + + if (recognizer is ISetLogicalParent logical) + logical.SetParent(null); + } + + return removed; + } + static readonly List s_Empty = new List(); public IEnumerator GetEnumerator() diff --git a/src/Avalonia.Base/Input/GestureRecognizers/SwipeGestureRecognizer.cs b/src/Avalonia.Base/Input/GestureRecognizers/SwipeGestureRecognizer.cs new file mode 100644 index 0000000000..2328e5e874 --- /dev/null +++ b/src/Avalonia.Base/Input/GestureRecognizers/SwipeGestureRecognizer.cs @@ -0,0 +1,225 @@ +using System; +using System.Diagnostics; +using Avalonia.Platform; + +namespace Avalonia.Input.GestureRecognizers +{ + /// + /// A gesture recognizer that detects swipe gestures for paging interactions. + /// + /// + /// Unlike , this recognizer is optimized for discrete + /// paging interactions (e.g., carousel navigation) rather than continuous scrolling. + /// It does not include inertia or friction physics. + /// + public class SwipeGestureRecognizer : GestureRecognizer + { + private bool _swiping; + private Point _trackedRootPoint; + private IPointer? _tracking; + private int _id; + + private Vector _velocity; + private long _lastTimestamp; + + /// + /// Defines the property. + /// + public static readonly StyledProperty CanHorizontallySwipeProperty = + AvaloniaProperty.Register(nameof(CanHorizontallySwipe)); + + /// + /// Defines the property. + /// + public static readonly StyledProperty CanVerticallySwipeProperty = + AvaloniaProperty.Register(nameof(CanVerticallySwipe)); + + /// + /// Defines the property. + /// + /// + /// A value of 0 (the default) causes the distance to be read from + /// at the time of the first gesture. + /// + public static readonly StyledProperty ThresholdProperty = + AvaloniaProperty.Register(nameof(Threshold), defaultValue: 0d); + + /// + /// Defines the property. + /// + public static readonly StyledProperty IsMouseEnabledProperty = + AvaloniaProperty.Register(nameof(IsMouseEnabled), defaultValue: false); + + /// + /// Defines the property. + /// + public static readonly StyledProperty IsEnabledProperty = + AvaloniaProperty.Register(nameof(IsEnabled), defaultValue: true); + + /// + /// Gets or sets a value indicating whether horizontal swipes are tracked. + /// + public bool CanHorizontallySwipe + { + get => GetValue(CanHorizontallySwipeProperty); + set => SetValue(CanHorizontallySwipeProperty, value); + } + + /// + /// Gets or sets a value indicating whether vertical swipes are tracked. + /// + public bool CanVerticallySwipe + { + get => GetValue(CanVerticallySwipeProperty); + set => SetValue(CanVerticallySwipeProperty, value); + } + + /// + /// Gets or sets the minimum pointer movement in pixels before a swipe is recognized. + /// A value of 0 reads the threshold from at gesture time. + /// + public double Threshold + { + get => GetValue(ThresholdProperty); + set => SetValue(ThresholdProperty, value); + } + + /// + /// Gets or sets a value indicating whether mouse pointer events trigger swipe gestures. + /// Defaults to ; touch and pen are always enabled. + /// + public bool IsMouseEnabled + { + get => GetValue(IsMouseEnabledProperty); + set => SetValue(IsMouseEnabledProperty, value); + } + + /// + /// Gets or sets a value indicating whether this recognizer responds to pointer events. + /// Defaults to . + /// + public bool IsEnabled + { + get => GetValue(IsEnabledProperty); + set => SetValue(IsEnabledProperty, value); + } + + /// + protected override void PointerPressed(PointerPressedEventArgs e) + { + if (!IsEnabled) + return; + + var point = e.GetCurrentPoint(null); + + if ((e.Pointer.Type is PointerType.Touch or PointerType.Pen || + (IsMouseEnabled && e.Pointer.Type == PointerType.Mouse)) + && point.Properties.IsLeftButtonPressed) + { + EndGesture(); + _tracking = e.Pointer; + _id = SwipeGestureEventArgs.GetNextFreeId(); + _trackedRootPoint = point.Position; + _velocity = default; + _lastTimestamp = 0; + } + } + + /// + protected override void PointerMoved(PointerEventArgs e) + { + if (e.Pointer == _tracking) + { + var rootPoint = e.GetPosition(null); + var threshold = GetEffectiveThreshold(); + + if (!_swiping) + { + var horizontalTriggered = CanHorizontallySwipe && Math.Abs(_trackedRootPoint.X - rootPoint.X) > threshold; + var verticalTriggered = CanVerticallySwipe && Math.Abs(_trackedRootPoint.Y - rootPoint.Y) > threshold; + + if (horizontalTriggered || verticalTriggered) + { + _swiping = true; + + _trackedRootPoint = new Point( + horizontalTriggered + ? _trackedRootPoint.X - (_trackedRootPoint.X >= rootPoint.X ? threshold : -threshold) + : rootPoint.X, + verticalTriggered + ? _trackedRootPoint.Y - (_trackedRootPoint.Y >= rootPoint.Y ? threshold : -threshold) + : rootPoint.Y); + + Capture(e.Pointer); + } + } + + if (_swiping) + { + var delta = _trackedRootPoint - rootPoint; + + var now = Stopwatch.GetTimestamp(); + if (_lastTimestamp > 0) + { + var elapsedSeconds = (double)(now - _lastTimestamp) / Stopwatch.Frequency; + if (elapsedSeconds > 0) + { + var instantVelocity = delta / elapsedSeconds; + _velocity = _velocity * 0.5 + instantVelocity * 0.5; + } + } + _lastTimestamp = now; + + Target!.RaiseEvent(new SwipeGestureEventArgs(_id, delta, _velocity)); + _trackedRootPoint = rootPoint; + e.Handled = true; + } + } + } + + /// + protected override void PointerCaptureLost(IPointer pointer) + { + if (pointer == _tracking) + EndGesture(); + } + + /// + protected override void PointerReleased(PointerReleasedEventArgs e) + { + if (e.Pointer == _tracking && _swiping) + { + e.Handled = true; + EndGesture(); + } + } + + private void EndGesture() + { + _tracking = null; + if (_swiping) + { + _swiping = false; + var endedArgs = new SwipeGestureEndedEventArgs(_id, _velocity); + _velocity = default; + _lastTimestamp = 0; + _id = 0; + Target!.RaiseEvent(endedArgs); + } + } + + private const double DefaultTapSize = 10; + + private double GetEffectiveThreshold() + { + var configured = Threshold; + if (configured > 0) + return configured; + + var tapSize = AvaloniaLocator.Current?.GetService() + ?.GetTapSize(PointerType.Touch).Height ?? DefaultTapSize; + + return tapSize / 2; + } + } +} diff --git a/src/Avalonia.Base/Input/Gestures.cs b/src/Avalonia.Base/Input/Gestures.cs index 3298af3a0f..07c9ab18be 100644 --- a/src/Avalonia.Base/Input/Gestures.cs +++ b/src/Avalonia.Base/Input/Gestures.cs @@ -1,15 +1,23 @@ using System; using System.Threading; using Avalonia.Interactivity; -using Avalonia.Platform; -using Avalonia.Threading; using Avalonia.Reactive; +using Avalonia.Threading; using Avalonia.VisualTree; namespace Avalonia.Input { - public static class Gestures + internal static class Gestures { + // These events are only used internally and not propagated through a route. + // They have routed event args as their target type because their args generated from + // PointerEventArgs. In order to prevent having identical classes with just their base class + // being different, these events use routed args. + public static event EventHandler? Holding; + public static event EventHandler? Tapped; + public static event EventHandler? RightTapped; + public static event EventHandler? DoubleTapped; + private record struct GestureState(GestureStateType Type, IPointer Pointer); private enum GestureStateType { @@ -22,202 +30,12 @@ namespace Avalonia.Input private static readonly WeakReference s_lastPress = new WeakReference(null); private static Point s_lastPressPoint; private static CancellationTokenSource? s_holdCancellationToken; - - /// - /// Defines the IsHoldingEnabled attached property. - /// - public static readonly AttachedProperty IsHoldingEnabledProperty = - AvaloniaProperty.RegisterAttached("IsHoldingEnabled", typeof(Gestures), true); - - /// - /// Defines the IsHoldWithMouseEnabled attached property. - /// - public static readonly AttachedProperty IsHoldWithMouseEnabledProperty = - AvaloniaProperty.RegisterAttached("IsHoldWithMouseEnabled", typeof(Gestures), false); - - public static readonly RoutedEvent TappedEvent = RoutedEvent.Register( - "Tapped", - RoutingStrategies.Bubble, - typeof(Gestures)); - - public static readonly RoutedEvent DoubleTappedEvent = RoutedEvent.Register( - "DoubleTapped", - RoutingStrategies.Bubble, - typeof(Gestures)); - - public static readonly RoutedEvent RightTappedEvent = RoutedEvent.Register( - "RightTapped", - RoutingStrategies.Bubble, - typeof(Gestures)); - - public static readonly RoutedEvent ScrollGestureEvent = - RoutedEvent.Register( - "ScrollGesture", RoutingStrategies.Bubble, typeof(Gestures)); - - public static readonly RoutedEvent ScrollGestureInertiaStartingEvent = - RoutedEvent.Register( - "ScrollGestureInertiaStarting", RoutingStrategies.Bubble, typeof(Gestures)); - - public static readonly RoutedEvent ScrollGestureEndedEvent = - RoutedEvent.Register( - "ScrollGestureEnded", RoutingStrategies.Bubble, typeof(Gestures)); - - public static readonly RoutedEvent PointerTouchPadGestureMagnifyEvent = - RoutedEvent.Register( - "PointerTouchPadGestureMagnify", RoutingStrategies.Bubble, typeof(Gestures)); - - public static readonly RoutedEvent PointerTouchPadGestureRotateEvent = - RoutedEvent.Register( - "PointerTouchPadGestureRotate", RoutingStrategies.Bubble, typeof(Gestures)); - - public static readonly RoutedEvent PointerTouchPadGestureSwipeEvent = - RoutedEvent.Register( - "PointerTouchPadGestureSwipe", RoutingStrategies.Bubble, typeof(Gestures)); - - public static readonly RoutedEvent PinchEvent = - RoutedEvent.Register( - "Pinch", RoutingStrategies.Bubble, typeof(Gestures)); - - public static readonly RoutedEvent PinchEndedEvent = - RoutedEvent.Register( - "PinchEnded", RoutingStrategies.Bubble, typeof(Gestures)); - - public static readonly RoutedEvent PullGestureEvent = - RoutedEvent.Register( - "PullGesture", RoutingStrategies.Bubble, typeof(Gestures)); - - /// - /// Occurs when a user performs a press and hold gesture (with a single touch, mouse, or pen/stylus contact). - /// - public static readonly RoutedEvent HoldingEvent = - RoutedEvent.Register( - "Holding", RoutingStrategies.Bubble, typeof(Gestures)); - - public static readonly RoutedEvent PullGestureEndedEvent = - RoutedEvent.Register( - "PullGestureEnded", RoutingStrategies.Bubble, typeof(Gestures)); - - public static bool GetIsHoldingEnabled(StyledElement element) - { - return element.GetValue(IsHoldingEnabledProperty); - } - public static void SetIsHoldingEnabled(StyledElement element, bool value) - { - element.SetValue(IsHoldingEnabledProperty, value); - } - - public static bool GetIsHoldWithMouseEnabled(StyledElement element) - { - return element.GetValue(IsHoldWithMouseEnabledProperty); - } - public static void SetIsHoldWithMouseEnabled(StyledElement element, bool value) - { - element.SetValue(IsHoldWithMouseEnabledProperty, value); - } - static Gestures() { InputElement.PointerPressedEvent.RouteFinished.Subscribe(PointerPressed); InputElement.PointerReleasedEvent.RouteFinished.Subscribe(PointerReleased); InputElement.PointerMovedEvent.RouteFinished.Subscribe(PointerMoved); } - - public static void AddTappedHandler(Interactive element, EventHandler handler) - { - element.AddHandler(TappedEvent, handler); - } - - public static void AddDoubleTappedHandler(Interactive element, EventHandler handler) - { - element.AddHandler(DoubleTappedEvent, handler); - } - - public static void AddRightTappedHandler(Interactive element, EventHandler handler) - { - element.AddHandler(RightTappedEvent, handler); - } - - public static void AddHoldingHandler(Interactive element, EventHandler handler) => - element.AddHandler(HoldingEvent, handler); - - public static void AddPinchHandler(Interactive element, EventHandler handler) => - element.AddHandler(PinchEvent, handler); - - public static void AddPinchEndedHandler(Interactive element, EventHandler handler) => - element.AddHandler(PinchEndedEvent, handler); - - public static void AddPullGestureHandler(Interactive element, EventHandler handler) => - element.AddHandler(PullGestureEvent, handler); - - public static void AddPullGestureEndedHandler(Interactive element, EventHandler handler) => - element.AddHandler(PullGestureEndedEvent, handler); - - public static void AddPointerTouchPadGestureMagnifyHandler(Interactive element, EventHandler handler) => - element.AddHandler(PointerTouchPadGestureMagnifyEvent, handler); - - public static void AddPointerTouchPadGestureRotateHandler(Interactive element, EventHandler handler) => - element.AddHandler(PointerTouchPadGestureRotateEvent, handler); - - public static void AddPointerTouchPadGestureSwipeHandler(Interactive element, EventHandler handler) => - element.AddHandler(PointerTouchPadGestureSwipeEvent, handler); - - public static void AddScrollGestureHandler(Interactive element, EventHandler handler) => - element.AddHandler(ScrollGestureEvent, handler); - - public static void AddScrollGestureEndedHandler(Interactive element, EventHandler handler) => - element.AddHandler(ScrollGestureEndedEvent, handler); - - public static void AddScrollGestureInertiaStartingHandler(Interactive element, EventHandler handler) => - element.AddHandler(ScrollGestureInertiaStartingEvent, handler); - - public static void RemoveTappedHandler(Interactive element, EventHandler handler) - { - element.RemoveHandler(TappedEvent, handler); - } - - public static void RemoveDoubleTappedHandler(Interactive element, EventHandler handler) - { - element.RemoveHandler(DoubleTappedEvent, handler); - } - - public static void RemoveRightTappedHandler(Interactive element, EventHandler handler) - { - element.RemoveHandler(RightTappedEvent, handler); - } - - public static void RemoveHoldingHandler(Interactive element, EventHandler handler) => - element.RemoveHandler(HoldingEvent, handler); - - public static void RemovePinchHandler(Interactive element, EventHandler handler) => - element.RemoveHandler(PinchEvent, handler); - - public static void RemovePinchEndedHandler(Interactive element, EventHandler handler) => - element.RemoveHandler(PinchEndedEvent, handler); - - public static void RemovePullGestureHandler(Interactive element, EventHandler handler) => - element.RemoveHandler(PullGestureEvent, handler); - - public static void RemovePullGestureEndedHandler(Interactive element, EventHandler handler) => - element.RemoveHandler(PullGestureEndedEvent, handler); - - public static void RemovePointerTouchPadGestureMagnifyHandler(Interactive element, EventHandler handler) => - element.RemoveHandler(PointerTouchPadGestureMagnifyEvent, handler); - - public static void RemovePointerTouchPadGestureRotateHandler(Interactive element, EventHandler handler) => - element.RemoveHandler(PointerTouchPadGestureRotateEvent, handler); - - public static void RemovePointerTouchPadGestureSwipeHandler(Interactive element, EventHandler handler) => - element.RemoveHandler(PointerTouchPadGestureSwipeEvent, handler); - - public static void RemoveScrollGestureHandler(Interactive element, EventHandler handler) => - element.RemoveHandler(ScrollGestureEvent,handler); - - public static void RemoveScrollGestureEndedHandler(Interactive element,EventHandler handler) => - element.RemoveHandler(ScrollGestureEndedEvent,handler); - - public static void RemoveScrollGestureInertiaStartingHandler(Interactive element, EventHandler handler) => - element.RemoveHandler(ScrollGestureInertiaStartingEvent, handler); - private static object? GetCaptured(RoutedEventArgs? args) { if (args is not PointerEventArgs pointerEventArgs) @@ -237,11 +55,11 @@ namespace Avalonia.Input var e = (PointerPressedEventArgs)ev; var visual = (Visual)source; - if(s_gestureState != null) + if (s_gestureState != null) { - if(s_gestureState.Value.Type == GestureStateType.Holding && source is Interactive i) + if (s_gestureState.Value.Type == GestureStateType.Holding && source is Interactive i) { - i.RaiseEvent(new HoldingRoutedEventArgs(HoldingState.Cancelled, s_lastPressPoint, s_gestureState.Value.Pointer.Type, e)); + Holding?.Invoke(i, new HoldingRoutedEventArgs(HoldingState.Canceled, s_lastPressPoint, s_gestureState.Value.Pointer.Type, e)); } s_holdCancellationToken?.Cancel(); s_holdCancellationToken?.Dispose(); @@ -257,28 +75,29 @@ namespace Avalonia.Input s_lastPressPoint = e.GetPosition((Visual)source); s_holdCancellationToken = new CancellationTokenSource(); var token = s_holdCancellationToken.Token; - var settings = ((IInputRoot?)visual.GetVisualRoot())?.PlatformSettings; + var settings = visual.GetPlatformSettings(); if (settings != null) { DispatcherTimer.RunOnce(() => { - if (s_gestureState != null && !token.IsCancellationRequested && source is InputElement i && GetIsHoldingEnabled(i) && (e.Pointer.Type != PointerType.Mouse || GetIsHoldWithMouseEnabled(i))) + if (s_gestureState != null && !token.IsCancellationRequested && source is InputElement i && InputElement.GetIsHoldingEnabled(i) && + (e.Pointer.Type != PointerType.Mouse || InputElement.GetIsHoldWithMouseEnabled(i))) { s_gestureState = new GestureState(GestureStateType.Holding, s_gestureState.Value.Pointer); - i.RaiseEvent(new HoldingRoutedEventArgs(HoldingState.Started, s_lastPressPoint, s_gestureState.Value.Pointer.Type, e)); + Holding?.Invoke(i, new HoldingRoutedEventArgs(HoldingState.Started, s_lastPressPoint, s_gestureState.Value.Pointer.Type, e)); } }, settings.HoldWaitDuration); } } else if (e.ClickCount % 2 == 0 && e.GetCurrentPoint(visual).Properties.IsLeftButtonPressed) { - if (s_lastPress.TryGetTarget(out var target) && - target == source && + if (s_lastPress.TryGetTarget(out var target) && + target == source && source is Interactive i) { s_gestureState = new GestureState(GestureStateType.DoubleTapped, e.Pointer); - i.RaiseEvent(new TappedEventArgs(DoubleTappedEvent, e)); + DoubleTapped?.Invoke(i, new TappedEventArgs(InputElement.DoubleTappedEvent, e)); } } } @@ -298,7 +117,7 @@ namespace Avalonia.Input source is Interactive i) { var point = e.GetCurrentPoint((Visual)target); - var settings = ((IInputRoot?)i.GetVisualRoot())?.PlatformSettings; + var settings = i.GetPlatformSettings(); var tapSize = settings?.GetTapSize(point.Pointer.Type) ?? new Size(4, 4); var tapRect = new Rect(s_lastPressPoint, new Size()) .Inflate(new Thickness(tapSize.Width, tapSize.Height)); @@ -307,17 +126,19 @@ namespace Avalonia.Input { if (s_gestureState?.Type == GestureStateType.Holding) { - i.RaiseEvent(new HoldingRoutedEventArgs(HoldingState.Completed, s_lastPressPoint, s_gestureState.Value.Pointer.Type, e)); + Holding?.Invoke(i, new HoldingRoutedEventArgs(HoldingState.Completed, s_lastPressPoint, s_gestureState.Value.Pointer.Type, e)); + + RightTapped?.Invoke(i, new TappedEventArgs(InputElement.RightTappedEvent, e)); } else if (e.InitialPressMouseButton == MouseButton.Right) { - i.RaiseEvent(new TappedEventArgs(RightTappedEvent, e)); + RightTapped?.Invoke(i, new TappedEventArgs(InputElement.RightTappedEvent, e)); } //GestureStateType.DoubleTapped needed here to prevent invoking Tapped event when DoubleTapped is called. //This behaviour matches UWP behaviour. else if (s_gestureState?.Type != GestureStateType.DoubleTapped) { - i.RaiseEvent(new TappedEventArgs(TappedEvent, e)); + Tapped?.Invoke(i, new TappedEventArgs(InputElement.TappedEvent, e)); } } s_gestureState = null; @@ -340,7 +161,6 @@ namespace Avalonia.Input if (e.Pointer == s_gestureState?.Pointer && source is Interactive i) { var point = e.GetCurrentPoint((Visual)target); - var settings = ((IInputRoot?)i.GetVisualRoot())?.PlatformSettings; var holdSize = new Size(4, 4); var holdRect = new Rect(s_lastPressPoint, new Size()) .Inflate(new Thickness(holdSize.Width, holdSize.Height)); @@ -352,15 +172,15 @@ namespace Avalonia.Input if (s_gestureState.Value.Type == GestureStateType.Holding) { - i.RaiseEvent(new HoldingRoutedEventArgs(HoldingState.Cancelled, s_lastPressPoint, s_gestureState.Value.Pointer.Type, e)); + Holding?.Invoke(i, new HoldingRoutedEventArgs(HoldingState.Canceled, s_lastPressPoint, s_gestureState.Value.Pointer.Type, e)); } + + s_holdCancellationToken?.Cancel(); + s_holdCancellationToken?.Dispose(); + s_holdCancellationToken = null; + s_gestureState = null; } } - - s_holdCancellationToken?.Cancel(); - s_holdCancellationToken?.Dispose(); - s_holdCancellationToken = null; - s_gestureState = null; } } } diff --git a/src/Avalonia.Base/Input/GotFocusEventArgs.cs b/src/Avalonia.Base/Input/GotFocusEventArgs.cs deleted file mode 100644 index 658bf5aae5..0000000000 --- a/src/Avalonia.Base/Input/GotFocusEventArgs.cs +++ /dev/null @@ -1,24 +0,0 @@ -using Avalonia.Interactivity; - -namespace Avalonia.Input -{ - /// - /// Holds arguments for a . - /// - public class GotFocusEventArgs : RoutedEventArgs, IKeyModifiersEventArgs - { - public GotFocusEventArgs() : base(InputElement.GotFocusEvent) - { - } - - /// - /// Gets or sets a value indicating how the change in focus occurred. - /// - public NavigationMethod NavigationMethod { get; init; } - - /// - /// Gets or sets any key modifiers active at the time of focus. - /// - public KeyModifiers KeyModifiers { get; init; } - } -} diff --git a/src/Avalonia.Base/Input/HoldingRoutedEventArgs.cs b/src/Avalonia.Base/Input/HoldingRoutedEventArgs.cs index efb0c01599..ffc5dfec70 100644 --- a/src/Avalonia.Base/Input/HoldingRoutedEventArgs.cs +++ b/src/Avalonia.Base/Input/HoldingRoutedEventArgs.cs @@ -6,7 +6,7 @@ namespace Avalonia.Input public class HoldingRoutedEventArgs : RoutedEventArgs { /// - /// Gets the state of the event. + /// Gets the state of the event. /// public HoldingState HoldingState { get; } @@ -20,25 +20,18 @@ namespace Avalonia.Input /// public PointerType PointerType { get; } - internal PointerEventArgs? PointerEventArgs { get; } + internal PointerEventArgs PointerEventArgs { get; } /// /// Initializes a new instance of the class. /// - public HoldingRoutedEventArgs(HoldingState holdingState, Point position, PointerType pointerType) : base(Gestures.HoldingEvent) + internal HoldingRoutedEventArgs(HoldingState holdingState, Point position, PointerType pointerType, PointerEventArgs pointerEventArgs) : base(InputElement.HoldingEvent) { + PointerEventArgs = pointerEventArgs; HoldingState = holdingState; Position = position; PointerType = pointerType; } - - /// - /// Initializes a new instance of the class. - /// - internal HoldingRoutedEventArgs(HoldingState holdingState, Point position, PointerType pointerType, PointerEventArgs pointerEventArgs) : this(holdingState, position, pointerType) - { - PointerEventArgs = pointerEventArgs; - } } public enum HoldingState @@ -56,6 +49,6 @@ namespace Avalonia.Input /// /// An additional contact is detected or a subsequent gesture (such as a slide) is detected. /// - Cancelled, + Canceled, } } diff --git a/src/Avalonia.Base/Input/IAccessKeyHandler.cs b/src/Avalonia.Base/Input/IAccessKeyHandler.cs index 418fa61f05..44dda96eb1 100644 --- a/src/Avalonia.Base/Input/IAccessKeyHandler.cs +++ b/src/Avalonia.Base/Input/IAccessKeyHandler.cs @@ -19,7 +19,7 @@ namespace Avalonia.Input /// /// This method can only be called once, typically by the owner itself on creation. /// - void SetOwner(IInputRoot owner); + void SetOwner(InputElement owner); /// /// Registers an input element to be associated with an access key. diff --git a/src/Avalonia.Base/Input/IFocusManager.cs b/src/Avalonia.Base/Input/IFocusManager.cs index 5691172f3f..d9e8d36f8b 100644 --- a/src/Avalonia.Base/Input/IFocusManager.cs +++ b/src/Avalonia.Base/Input/IFocusManager.cs @@ -14,9 +14,60 @@ namespace Avalonia.Input IInputElement? GetFocusedElement(); /// - /// Clears currently focused element. + /// Focuses a control. /// - [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(); + /// The control to focus. + /// The method by which focus was changed. + /// Any key modifiers active at the time of focus. + /// true if the focus moved to a control; otherwise, false. + /// + /// If 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 true, it is not guaranteed that the focus has been moved + /// to . The focus might have been redirected to another element. + /// + bool Focus( + IInputElement? element, + NavigationMethod method = NavigationMethod.Unspecified, + KeyModifiers keyModifiers = KeyModifiers.None); + + /// + /// Attempts to change focus from the element with focus to the next focusable element in the specified direction. + /// + /// + /// The direction that focus moves from element to element. + /// Must be one of , , + /// , , + /// and . + /// + /// The options to help identify the next element to receive focus. + /// true if focus moved; otherwise, false. + bool TryMoveFocus(NavigationDirection direction, FindNextElementOptions? options = null); + + /// + /// Retrieves the first element that can receive focus. + /// + /// The first focusable element. + IInputElement? FindFirstFocusableElement(); + + /// + /// Retrieves the last element that can receive focus. + /// + /// The last focusable element. + IInputElement? FindLastFocusableElement(); + + /// + /// Retrieves the element that should receive focus based on the specified navigation direction. + /// + /// + /// The direction that focus moves from element to element. + /// Must be one of , , + /// , , + /// and . + /// + /// The options to help identify the next element to receive focus. + /// The next element to receive focus, if any. + IInputElement? FindNextElement(NavigationDirection direction, FindNextElementOptions? options = null); } } diff --git a/src/Avalonia.Base/Input/IInputElement.cs b/src/Avalonia.Base/Input/IInputElement.cs index 39dc30befd..f69f7ea343 100644 --- a/src/Avalonia.Base/Input/IInputElement.cs +++ b/src/Avalonia.Base/Input/IInputElement.cs @@ -14,12 +14,12 @@ namespace Avalonia.Input /// /// Occurs when the control receives focus. /// - event EventHandler? GotFocus; + event EventHandler? GotFocus; /// /// Occurs when the control loses focus. /// - event EventHandler? LostFocus; + event EventHandler? LostFocus; /// /// Occurs when a key is pressed while the control has focus. diff --git a/src/Avalonia.Base/Input/IInputRoot.cs b/src/Avalonia.Base/Input/IInputRoot.cs index c1c5968ebe..13b7bae813 100644 --- a/src/Avalonia.Base/Input/IInputRoot.cs +++ b/src/Avalonia.Base/Input/IInputRoot.cs @@ -1,3 +1,4 @@ +using Avalonia.Input.TextInput; using Avalonia.Metadata; using Avalonia.Platform; @@ -6,38 +7,43 @@ namespace Avalonia.Input /// /// Defines the interface for top-level input elements. /// - [NotClientImplementable] - public interface IInputRoot : IInputElement + [PrivateApi] + public interface IInputRoot { - /// - /// Gets or sets the keyboard navigation handler. - /// - IKeyboardNavigationHandler? KeyboardNavigationHandler { get; } - /// /// Gets focus manager of the root. /// /// /// Focus manager can be null only if window wasn't initialized yet. /// - IFocusManager? FocusManager { get; } - - /// - /// Represents a contract for accessing top-level platform-specific settings. - /// - /// - /// PlatformSettings can be null only if window wasn't initialized yet. - /// - IPlatformSettings? PlatformSettings { get; } + public IFocusManager? FocusManager { get; } /// /// Gets or sets the input element that the pointer is currently over. /// - IInputElement? PointerOverElement { get; set; } - + internal IInputElement? PointerOverElement { get; set; } + + internal ITextInputMethodImpl? InputMethod { get; } + + internal InputElement RootElement { get; } + + // HACK: This is a temporary hack for "default focus" concept. + // If nothing is focused we send keyboard events to Window. Since for now we always + // control PresentationSource, we simply pass the TopLevel as a separate parameter there. + // It's also currently used by automation since we have special WindowAutomationPeer which needs to target the + // window itself + public InputElement FocusRoot { get; } + /// - /// Gets or sets a value indicating whether access keys are shown in the window. + /// Performs a hit-test for chrome/decoration elements at the given position. /// - bool ShowAccessKeys { get; set; } + /// The point in root-relative coordinates. + /// + /// null if no chrome element was hit (no chrome involvement at this point). + /// or + /// if an interactive chrome element was hit — the platform should redirect non-client input to regular client input. + /// Any other non- value indicates a specific non-client role (titlebar, resize grip, etc.). + /// + internal WindowDecorationsElementRole? HitTestChromeElement(Point point) => null; } } diff --git a/src/Avalonia.Base/Input/IKeyboardNavigationHandler.cs b/src/Avalonia.Base/Input/IKeyboardNavigationHandler.cs index e82bb5d216..5e5cac0c0b 100644 --- a/src/Avalonia.Base/Input/IKeyboardNavigationHandler.cs +++ b/src/Avalonia.Base/Input/IKeyboardNavigationHandler.cs @@ -6,7 +6,7 @@ namespace Avalonia.Input /// Defines the interface for classes that handle keyboard navigation for a window. /// [Unstable] - public interface IKeyboardNavigationHandler + internal interface IKeyboardNavigationHandler { /// /// Sets the owner of the keyboard navigation handler. @@ -16,7 +16,7 @@ namespace Avalonia.Input /// This method can only be called once, typically by the owner itself on creation. /// [PrivateApi] - void SetOwner(IInputRoot owner); + void SetOwner(InputElement owner); /// /// Moves the focus in the specified direction. diff --git a/src/Avalonia.Base/Input/InputElement.Gestures.cs b/src/Avalonia.Base/Input/InputElement.Gestures.cs new file mode 100644 index 0000000000..1323e4d35e --- /dev/null +++ b/src/Avalonia.Base/Input/InputElement.Gestures.cs @@ -0,0 +1,335 @@ +using System; +using Avalonia.Interactivity; + +namespace Avalonia.Input +{ + public partial class InputElement + { + private bool _isContextMenuOnHolding; + + /// + /// Defines the IsHoldingEnabled attached property. + /// + public static readonly AttachedProperty IsHoldingEnabledProperty = + AvaloniaProperty.RegisterAttached("IsHoldingEnabled", typeof(InputElement), true); + + /// + /// Defines the IsHoldWithMouseEnabled attached property. + /// + public static readonly AttachedProperty IsHoldWithMouseEnabledProperty = + AvaloniaProperty.RegisterAttached("IsHoldWithMouseEnabled", typeof(InputElement), false); + + /// + /// Defines the event. + /// + public static readonly RoutedEvent PinchEvent = + RoutedEvent.Register( + nameof(Pinch), RoutingStrategies.Bubble); + + /// + /// Defines the event. + /// + public static readonly RoutedEvent PinchEndedEvent = + RoutedEvent.Register( + nameof(PinchEnded), RoutingStrategies.Bubble); + + /// + /// Defines the event. + /// + public static readonly RoutedEvent PullGestureEvent = + RoutedEvent.Register( + nameof(PullGesture), RoutingStrategies.Bubble); + + /// + /// Defines the event. + /// + public static readonly RoutedEvent PullGestureEndedEvent = + RoutedEvent.Register( + nameof(PullGestureEnded), RoutingStrategies.Bubble); + + /// + /// Defines the event. + /// + public static readonly RoutedEvent SwipeGestureEvent = + RoutedEvent.Register( + nameof(SwipeGesture), RoutingStrategies.Bubble); + + /// + /// Defines the event. + /// + public static readonly RoutedEvent SwipeGestureEndedEvent = + RoutedEvent.Register( + nameof(SwipeGestureEnded), RoutingStrategies.Bubble); + + /// + /// Defines the event. + /// + public static readonly RoutedEvent ScrollGestureEvent = + RoutedEvent.Register( + nameof(ScrollGesture), RoutingStrategies.Bubble); + + /// + /// Defines the event. + /// + public static readonly RoutedEvent ScrollGestureInertiaStartingEvent = + RoutedEvent.Register( + nameof(ScrollGestureInertiaStarting), RoutingStrategies.Bubble); + + /// + /// Defines the event. + /// + public static readonly RoutedEvent ScrollGestureEndedEvent = + RoutedEvent.Register( + nameof(ScrollGestureEnded), RoutingStrategies.Bubble); + + /// + /// Defines the event. + /// + public static readonly RoutedEvent PointerTouchPadGestureMagnifyEvent = + RoutedEvent.Register( + nameof(PointerTouchPadGestureMagnify), RoutingStrategies.Bubble); + + /// + /// Defines the event. + /// + public static readonly RoutedEvent PointerTouchPadGestureRotateEvent = + RoutedEvent.Register( + nameof(PointerTouchPadGestureRotate), RoutingStrategies.Bubble); + + /// + /// Defines the event. + /// + public static readonly RoutedEvent PointerTouchPadGestureSwipeEvent = + RoutedEvent.Register( + nameof(PointerTouchPadGestureSwipe), RoutingStrategies.Bubble); + + /// + /// Defines the event. + /// + public static readonly RoutedEvent TappedEvent = + RoutedEvent.Register( + nameof(Tapped), + RoutingStrategies.Bubble); + + /// + /// Defines the event. + /// + public static readonly RoutedEvent RightTappedEvent = + RoutedEvent.Register( + nameof(RightTapped), + RoutingStrategies.Bubble); + + /// + /// Defines the event. + /// + public static readonly RoutedEvent HoldingEvent = + RoutedEvent.Register( + nameof(Holding), + RoutingStrategies.Bubble); + + /// + /// Defines the event. + /// + public static readonly RoutedEvent DoubleTappedEvent = + RoutedEvent.Register( + nameof(DoubleTapped), + RoutingStrategies.Bubble); + + public static bool GetIsHoldingEnabled(StyledElement element) + { + return element.GetValue(IsHoldingEnabledProperty); + } + + public static void SetIsHoldingEnabled(StyledElement element, bool value) + { + element.SetValue(IsHoldingEnabledProperty, value); + } + + public static bool GetIsHoldWithMouseEnabled(StyledElement element) + { + return element.GetValue(IsHoldWithMouseEnabledProperty); + } + + public static void SetIsHoldWithMouseEnabled(StyledElement element, bool value) + { + element.SetValue(IsHoldWithMouseEnabledProperty, value); + } + + /// + /// Occurs when the user moves two contact points closer together. + /// + public event EventHandler? Pinch + { + add { AddHandler(PinchEvent, value); } + remove { RemoveHandler(PinchEvent, value); } + } + + /// + /// Occurs when the user releases both contact points used in a pinch gesture. + /// + public event EventHandler? PinchEnded + { + add { AddHandler(PinchEndedEvent, value); } + remove { RemoveHandler(PinchEndedEvent, value); } + } + + /// + /// Occurs when the user drags from the edge of a control. + /// + public event EventHandler? PullGesture + { + add { AddHandler(PullGestureEvent, value); } + remove { RemoveHandler(PullGestureEvent, value); } + } + + /// + /// Occurs when the user releases the pointer after a pull gesture. + /// + public event EventHandler? PullGestureEnded + { + add { AddHandler(PullGestureEndedEvent, value); } + remove { RemoveHandler(PullGestureEndedEvent, value); } + } + + /// + /// Occurs when the user continuously moves the pointer in the same direction within the control’s boundaries. + /// + public event EventHandler? ScrollGesture + { + add { AddHandler(ScrollGestureEvent, value); } + remove { RemoveHandler(ScrollGestureEvent, value); } + } + + /// + /// Occurs within a scroll gesture, when the user releases the pointer, and scrolling continues by transitioning to momentum-based gliding movement. + /// + public event EventHandler? ScrollGestureInertiaStarting + { + add { AddHandler(ScrollGestureInertiaStartingEvent, value); } + remove { RemoveHandler(ScrollGestureInertiaStartingEvent, value); } + } + + /// + /// 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. + /// + public event EventHandler? ScrollGestureEnded + { + add { AddHandler(ScrollGestureEndedEvent, value); } + remove { RemoveHandler(ScrollGestureEndedEvent, value); } + } + + /// + /// Occurs when the user moves two contact points away from each other on a touchpad. + /// + public event EventHandler? PointerTouchPadGestureMagnify + { + add { AddHandler(PointerTouchPadGestureMagnifyEvent, value); } + remove { RemoveHandler(PointerTouchPadGestureMagnifyEvent, value); } + } + + /// + /// Occurs when the user places two contact points and moves them in a circular motion on a touchpad. + /// + public event EventHandler? PointerTouchPadGestureRotate + { + add { AddHandler(PointerTouchPadGestureRotateEvent, value); } + remove { RemoveHandler(PointerTouchPadGestureRotateEvent, value); } + } + + /// + /// Occurs when the user rapidly drags the pointer in a single direction across the control. + /// + public event EventHandler? SwipeGesture + { + add { AddHandler(SwipeGestureEvent, value); } + remove { RemoveHandler(SwipeGestureEvent, value); } + } + + /// + /// Occurs when a swipe gesture ends on the control. + /// + public event EventHandler? SwipeGestureEnded + { + add { AddHandler(SwipeGestureEndedEvent, value); } + remove { RemoveHandler(SwipeGestureEndedEvent, value); } + } + + /// + /// Occurs when the user performs a rapid dragging motion in a single direction on a touchpad. + /// + public event EventHandler? PointerTouchPadGestureSwipe + { + add { AddHandler(PointerTouchPadGestureSwipeEvent, value); } + remove { RemoveHandler(PointerTouchPadGestureSwipeEvent, value); } + } + + /// + /// Occurs when the user briefly contacts and releases a single point, without significant movement. + /// + public event EventHandler? Tapped + { + add { AddHandler(TappedEvent, value); } + remove { RemoveHandler(TappedEvent, value); } + } + + /// + /// 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. + /// + public event EventHandler? RightTapped + { + add { AddHandler(RightTappedEvent, value); } + remove { RemoveHandler(RightTappedEvent, value); } + } + + /// + /// Occurs when the user makes a single contact, then maintains contact beyond a given time threshold without releasing or making another contact. + /// + public event EventHandler? Holding + { + add { AddHandler(HoldingEvent, value); } + remove { RemoveHandler(HoldingEvent, value); } + } + + /// + /// Occurs when the user briefly contacts and releases twice on a single point, without significant movement. + /// + public event EventHandler? DoubleTapped + { + add { AddHandler(DoubleTappedEvent, value); } + remove { RemoveHandler(DoubleTappedEvent, value); } + } + + private static void OnPreviewHolding(object? sender, HoldingRoutedEventArgs e) + { + if (sender is InputElement inputElement) + { + inputElement.RaiseEvent(e); + + if (!e.Handled && e.HoldingState == HoldingState.Started) + { + var contextEvent = new ContextRequestedEventArgs(e.PointerEventArgs); + inputElement.RaiseEvent(contextEvent); + e.Handled = contextEvent.Handled; + + if (contextEvent.Handled) + { + inputElement._isContextMenuOnHolding = true; + } + } + else if (e.HoldingState == HoldingState.Canceled && inputElement._isContextMenuOnHolding) + { + inputElement.RaiseEvent(new RoutedEventArgs(InputElement.ContextCanceledEvent) + { + Source = inputElement + }); + + inputElement._isContextMenuOnHolding = false; + } + else if (e.HoldingState == HoldingState.Completed) + { + inputElement._isContextMenuOnHolding = false; + } + } + } + } +} diff --git a/src/Avalonia.Base/Input/InputElement.cs b/src/Avalonia.Base/Input/InputElement.cs index d15abfbd3f..e908e818e8 100644 --- a/src/Avalonia.Base/Input/InputElement.cs +++ b/src/Avalonia.Base/Input/InputElement.cs @@ -16,7 +16,7 @@ namespace Avalonia.Input /// Implements input-related functionality for a control. /// [PseudoClasses(":disabled", ":focus", ":focus-visible", ":focus-within", ":pointerover")] - public class InputElement : Interactive, IInputElement + public partial class InputElement : Interactive, IInputElement { /// /// Defines the property. @@ -79,8 +79,8 @@ namespace Avalonia.Input /// /// Defines the event. /// - public static readonly RoutedEvent GotFocusEvent = - RoutedEvent.Register(nameof(GotFocus), RoutingStrategies.Bubble); + public static readonly RoutedEvent GotFocusEvent = + RoutedEvent.Register(nameof(GotFocus), RoutingStrategies.Bubble); /// /// Defines the event. @@ -91,8 +91,8 @@ namespace Avalonia.Input /// /// Defines the event. /// - public static readonly RoutedEvent LostFocusEvent = - RoutedEvent.Register(nameof(LostFocus), RoutingStrategies.Bubble); + public static readonly RoutedEvent LostFocusEvent = + RoutedEvent.Register(nameof(LostFocus), RoutingStrategies.Bubble); /// /// Defines the event. @@ -203,24 +203,20 @@ namespace Avalonia.Input RoutingStrategies.Tunnel | RoutingStrategies.Bubble); /// - /// Defines the event. + /// Provides event data for the event. /// - public static readonly RoutedEvent TappedEvent = Gestures.TappedEvent; - - /// - /// Defines the event. - /// - public static readonly RoutedEvent RightTappedEvent = Gestures.RightTappedEvent; - - /// - /// Defines the event. - /// - public static readonly RoutedEvent HoldingEvent = Gestures.HoldingEvent; + public static readonly RoutedEvent ContextRequestedEvent = + RoutedEvent.Register( + nameof(ContextRequested), + RoutingStrategies.Tunnel | RoutingStrategies.Bubble); /// - /// Defines the event. + /// Provides event data for the event. /// - public static readonly RoutedEvent DoubleTappedEvent = Gestures.DoubleTappedEvent; + public static readonly RoutedEvent ContextCanceledEvent = + RoutedEvent.Register( + nameof(ContextCanceled), + RoutingStrategies.Tunnel | RoutingStrategies.Bubble); private bool _isEffectivelyEnabled = true; private bool _isFocused; @@ -257,6 +253,12 @@ namespace Avalonia.Input DoubleTappedEvent.AddClassHandler((x, e) => x.OnDoubleTapped(e)); HoldingEvent.AddClassHandler((x, e) => x.OnHolding(e)); + Gestures.Tapped += (s, e) => (s as InputElement)?.RaiseEvent(e); + Gestures.RightTapped += (s, e) => (s as InputElement)?.RaiseEvent(e); + Gestures.DoubleTapped += (s, e) => (s as InputElement)?.RaiseEvent(e); + + Gestures.Holding += OnPreviewHolding; + // Gesture only handlers PointerMovedEvent.AddClassHandler((x, e) => x.OnGesturePointerMoved(e), handledEventsToo: true); PointerPressedEvent.AddClassHandler((x, e) => x.OnGesturePointerPressed(e), handledEventsToo: true); @@ -276,7 +278,7 @@ namespace Avalonia.Input /// /// Occurs when the control receives focus. /// - public event EventHandler? GotFocus + public event EventHandler? GotFocus { add { AddHandler(GotFocusEvent, value); } remove { RemoveHandler(GotFocusEvent, value); } @@ -294,7 +296,7 @@ namespace Avalonia.Input /// /// Occurs when the control loses focus. /// - public event EventHandler? LostFocus + public event EventHandler? LostFocus { add { AddHandler(LostFocusEvent, value); } remove { RemoveHandler(LostFocusEvent, value); } @@ -419,42 +421,6 @@ namespace Avalonia.Input remove { RemoveHandler(PointerWheelChangedEvent, value); } } - /// - /// Occurs when a tap gesture occurs on the control. - /// - public event EventHandler? Tapped - { - add { AddHandler(TappedEvent, value); } - remove { RemoveHandler(TappedEvent, value); } - } - - /// - /// Occurs when a right tap gesture occurs on the control. - /// - public event EventHandler? RightTapped - { - add { AddHandler(RightTappedEvent, value); } - remove { RemoveHandler(RightTappedEvent, value); } - } - - /// - /// Occurs when a hold gesture occurs on the control. - /// - public event EventHandler? Holding - { - add { AddHandler(HoldingEvent, value); } - remove { RemoveHandler(HoldingEvent, value); } - } - - /// - /// Occurs when a double-tap gesture occurs on the control. - /// - public event EventHandler? DoubleTapped - { - add { AddHandler(DoubleTappedEvent, value); } - remove { RemoveHandler(DoubleTappedEvent, value); } - } - /// /// Gets or sets a value indicating whether the control can receive focus. /// @@ -518,6 +484,24 @@ namespace Avalonia.Input internal set { SetAndRaise(IsPointerOverProperty, ref _isPointerOver, value); } } + /// + /// Occurs when the user has completed a context input gesture, such as a right-click. + /// + public event EventHandler? ContextRequested + { + add => AddHandler(ContextRequestedEvent, value); + remove => RemoveHandler(ContextRequestedEvent, value); + } + + /// + /// Occurs when the context input gesture continues into another gesture, to notify the element that the context flyout should not be opened. + /// + public event EventHandler? ContextCanceled + { + add => AddHandler(ContextCanceledEvent, value); + remove => RemoveHandler(ContextCanceledEvent, value); + } + /// /// Gets or sets a value that indicates whether the control is included in tab navigation. /// @@ -539,7 +523,7 @@ namespace Avalonia.Input if (!IsEffectivelyEnabled && FocusManager.GetFocusManager(this) is { } focusManager && Equals(focusManager.GetFocusedElement(), this)) { - focusManager.ClearFocus(); + focusManager.Focus(null); } } } @@ -583,7 +567,9 @@ namespace Avalonia.Input if (IsFocused) { - FocusManager.GetFocusManager(e.Root as IInputElement)?.ClearFocusOnElementRemoved(this, e.Parent); + var root = e.AttachmentPoint ?? e.RootVisual; + ((FocusManager?)e.PresentationSource.InputRoot.FocusManager) + ?.ClearFocusOnElementRemoved(this, root); } IsKeyboardFocusWithin = false; @@ -607,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); @@ -631,11 +617,11 @@ namespace Avalonia.Input /// for this event. /// /// Data about the event. - 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; @@ -648,7 +634,7 @@ namespace Avalonia.Input /// for this event. /// /// Data about the event. - protected virtual void OnLostFocus(RoutedEventArgs e) + protected virtual void OnLostFocus(FocusChangedEventArgs e) { } @@ -1009,7 +995,7 @@ namespace Avalonia.Input } else { - focusManager.ClearFocus(); + focusManager.Focus(null); } } } diff --git a/src/Avalonia.Base/Input/InputManager.cs b/src/Avalonia.Base/Input/InputManager.cs index c9b1751b2a..7f5b5f82e7 100644 --- a/src/Avalonia.Base/Input/InputManager.cs +++ b/src/Avalonia.Base/Input/InputManager.cs @@ -8,7 +8,7 @@ namespace Avalonia.Input /// Receives input from the windowing subsystem and dispatches it to interested parties /// for processing. /// - internal class InputManager : IInputManager + internal class InputManager : IInputManager, IDisposable { private readonly LightweightSubject _preProcess = new(); private readonly LightweightSubject _process = new(); @@ -36,5 +36,12 @@ namespace Avalonia.Input _process.OnNext(e); _postProcess.OnNext(e); } + + public void Dispose() + { + _preProcess.OnCompleted(); + _process.OnCompleted(); + _postProcess.OnCompleted(); + } } } diff --git a/src/Avalonia.Base/Input/KeyGesture.cs b/src/Avalonia.Base/Input/KeyGesture.cs index 83d99bf7a9..463337ddda 100644 --- a/src/Avalonia.Base/Input/KeyGesture.cs +++ b/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(modifier.ToString(), true); + return Enum.Parse(modifier.ToString(), true); } private static Key ResolveNumPadOperationKey(Key key) diff --git a/src/Avalonia.Base/Input/KeyboardDevice.cs b/src/Avalonia.Base/Input/KeyboardDevice.cs index 3971ef9364..e2b530a91a 100644 --- a/src/Avalonia.Base/Input/KeyboardDevice.cs +++ b/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 || - _focusedRoot != ((Visual?)element)?.VisualRoot as IInputRoot) && + if (oldElement != null && + (!((Visual)oldElement).IsAttachedToVisualTree || + _focusedRoot != ((Visual?)element)?.GetInputRoot()) && _focusedRoot != null) { - ClearChildrenFocusWithin(_focusedRoot, true); + ClearChildrenFocusWithin(_focusedRoot.RootElement, true); } - SetIsFocusWithin(FocusedElement, element); + SetIsFocusWithin(oldElement, element); _focusedElement = element; - _focusedRoot = ((Visual?)_focusedElement)?.VisualRoot as IInputRoot; + _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); @@ -225,7 +235,7 @@ namespace Avalonia.Input if(e.Handled) return; - var element = FocusedElement ?? e.Root; + var element = FocusedElement ?? e.Root.FocusRoot; if (e is RawKeyEventArgs keyInput) { diff --git a/src/Avalonia.Base/Input/KeyboardNavigationHandler.cs b/src/Avalonia.Base/Input/KeyboardNavigationHandler.cs index e5e7eb0699..88a293f6bb 100644 --- a/src/Avalonia.Base/Input/KeyboardNavigationHandler.cs +++ b/src/Avalonia.Base/Input/KeyboardNavigationHandler.cs @@ -10,13 +10,12 @@ namespace Avalonia.Input /// /// Handles keyboard navigation for a window. /// - [Unstable] - public sealed class KeyboardNavigationHandler : IKeyboardNavigationHandler + internal sealed class KeyboardNavigationHandler : IKeyboardNavigationHandler { /// /// The window to which the handler belongs. /// - private IInputRoot? _owner; + private InputElement? _owner; /// /// Sets the owner of the keyboard navigation handler. @@ -26,7 +25,7 @@ namespace Avalonia.Input /// This method can only be called once, typically by the owner itself on creation. /// [PrivateApi] - public void SetOwner(IInputRoot owner) + public void SetOwner(InputElement owner) { if (_owner != null) { @@ -56,7 +55,7 @@ namespace Avalonia.Input private static IInputElement? GetNextPrivate( IInputElement? element, - IInputRoot? owner, + InputElement? owner, NavigationDirection direction, KeyDeviceType? keyDeviceType) { diff --git a/src/Avalonia.Base/Input/MouseDevice.cs b/src/Avalonia.Base/Input/MouseDevice.cs index 62105c7deb..2d112efda1 100644 --- a/src/Avalonia.Base/Input/MouseDevice.cs +++ b/src/Avalonia.Base/Input/MouseDevice.cs @@ -1,13 +1,10 @@ using System; using System.Collections.Generic; -using Avalonia.Reactive; +using Avalonia.Input.GestureRecognizers; using Avalonia.Input.Raw; using Avalonia.Interactivity; using Avalonia.Metadata; -using Avalonia.Platform; -using Avalonia.Utilities; using Avalonia.VisualTree; -using Avalonia.Input.GestureRecognizers; #pragma warning disable CS0618 namespace Avalonia.Input @@ -135,20 +132,20 @@ namespace Avalonia.Input return new PointerPointProperties(args.InputModifiers, args.Type.ToUpdateKind()); } - private bool MouseDown(IMouseDevice device, ulong timestamp, IInputElement root, Point p, + private bool MouseDown(IMouseDevice device, ulong timestamp, IInputRoot root, Point p, PointerPointProperties properties, KeyModifiers inputModifiers, IInputElement? hitTest) { device = device ?? throw new ArgumentNullException(nameof(device)); root = root ?? throw new ArgumentNullException(nameof(root)); - var source = _pointer.Captured ?? root.InputHitTest(p); + var source = _pointer.Captured ?? root.RootElement.InputHitTest(p); if (source != null) { _pointer.Capture(source, CaptureSource.Implicit); - var settings = ((IInputRoot?)(source as Interactive)?.GetVisualRoot())?.PlatformSettings; + var settings = (source as Interactive)?.GetPlatformSettings(); if (settings is not null) { var doubleClickTime = settings.GetDoubleTapTime(PointerType.Mouse).TotalMilliseconds; @@ -166,7 +163,7 @@ namespace Avalonia.Input } _lastMouseDownButton = properties.PointerUpdateKind.GetMouseButton(); - var e = new PointerPressedEventArgs(source, _pointer, (Visual)root, p, timestamp, properties, inputModifiers, _clickCount); + var e = new PointerPressedEventArgs(source, _pointer, root.RootElement, p, timestamp, properties, inputModifiers, _clickCount); source.RaiseEvent(e); return e.Handled; } @@ -185,7 +182,7 @@ namespace Avalonia.Input if (source is object) { - var e = new PointerEventArgs(InputElement.PointerMovedEvent, source, _pointer, (Visual)root, + var e = new PointerEventArgs(InputElement.PointerMovedEvent, source, _pointer, root.RootElement, p, timestamp, properties, inputModifiers, intermediatePoints); if (_pointer.CapturedGestureRecognizer is GestureRecognizer gestureRecognizer) @@ -209,7 +206,7 @@ namespace Avalonia.Input if (source is not null) { - var e = new PointerReleasedEventArgs(source, _pointer, (Visual)root, p, timestamp, props, inputModifiers, + var e = new PointerReleasedEventArgs(source, _pointer, root.RootElement, p, timestamp, props, inputModifiers, _lastMouseDownButton); try @@ -244,7 +241,7 @@ namespace Avalonia.Input if (source is not null) { - var e = new PointerWheelEventArgs(source, _pointer, (Visual)root, p, timestamp, props, inputModifiers, delta); + var e = new PointerWheelEventArgs(source, _pointer, root.RootElement, p, timestamp, props, inputModifiers, delta); source?.RaiseEvent(e); return e.Handled; @@ -263,8 +260,8 @@ namespace Avalonia.Input if (source != null) { - var e = new PointerDeltaEventArgs(Gestures.PointerTouchPadGestureMagnifyEvent, source, - _pointer, (Visual)root, p, timestamp, props, inputModifiers, delta); + var e = new PointerDeltaEventArgs(InputElement.PointerTouchPadGestureMagnifyEvent, source, + _pointer, root.RootElement, p, timestamp, props, inputModifiers, delta); source?.RaiseEvent(e); return e.Handled; @@ -283,8 +280,8 @@ namespace Avalonia.Input if (source != null) { - var e = new PointerDeltaEventArgs(Gestures.PointerTouchPadGestureRotateEvent, source, - _pointer, (Visual)root, p, timestamp, props, inputModifiers, delta); + var e = new PointerDeltaEventArgs(InputElement.PointerTouchPadGestureRotateEvent, source, + _pointer, root.RootElement, p, timestamp, props, inputModifiers, delta); source?.RaiseEvent(e); return e.Handled; @@ -303,8 +300,8 @@ namespace Avalonia.Input if (source != null) { - var e = new PointerDeltaEventArgs(Gestures.PointerTouchPadGestureSwipeEvent, source, - _pointer, (Visual)root, p, timestamp, props, inputModifiers, delta); + var e = new PointerDeltaEventArgs(InputElement.PointerTouchPadGestureSwipeEvent, source, + _pointer, root.RootElement, p, timestamp, props, inputModifiers, delta); source?.RaiseEvent(e); return e.Handled; diff --git a/src/Avalonia.Base/Input/Navigation/XYFocus.FindElements.cs b/src/Avalonia.Base/Input/Navigation/XYFocus.FindElements.cs index 0f529142ca..ed93b86475 100644 --- a/src/Avalonia.Base/Input/Navigation/XYFocus.FindElements.cs +++ b/src/Avalonia.Base/Input/Navigation/XYFocus.FindElements.cs @@ -105,12 +105,11 @@ public partial class XYFocus private static bool IsOccluded(InputElement element, Rect elementBounds) { - // if (element is CHyperlink hyperlink) - // { - // element = hyperlink.GetContainingFrameworkElement(); - // } - - var root = (InputElement)element.GetVisualRoot()!; + // TODO: The check for bounds is no longer correct + + var root = (InputElement?)element.VisualRoot; + if (root == null) + return true; // Check if the element is within the visible area of the window var visibleBounds = new Rect(0, 0, root.Bounds.Width, root.Bounds.Height); diff --git a/src/Avalonia.Base/Input/Navigation/XYFocus.Impl.cs b/src/Avalonia.Base/Input/Navigation/XYFocus.Impl.cs index 929d92a650..7aab6d8b65 100644 --- a/src/Avalonia.Base/Input/Navigation/XYFocus.Impl.cs +++ b/src/Avalonia.Base/Input/Navigation/XYFocus.Impl.cs @@ -117,7 +117,9 @@ public partial class XYFocus { if (element == null) return null; - var root = (InputElement)element.GetVisualRoot()!; + var root = (InputElement?)element.VisualRoot; + if (root == null) + return null; var isRightToLeft = element.FlowDirection == FlowDirection.RightToLeft; var mode = GetStrategy(element, direction, xyFocusOptions.NavigationStrategyOverride); diff --git a/src/Avalonia.Base/Input/Navigation/XYFocusOptions.cs b/src/Avalonia.Base/Input/Navigation/XYFocusOptions.cs index 4bfcb22502..8e4c847aa9 100644 --- a/src/Avalonia.Base/Input/Navigation/XYFocusOptions.cs +++ b/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; + } } diff --git a/src/Avalonia.Base/Input/PenDevice.cs b/src/Avalonia.Base/Input/PenDevice.cs index 3f168f66b0..b3301ce612 100644 --- a/src/Avalonia.Base/Input/PenDevice.cs +++ b/src/Avalonia.Base/Input/PenDevice.cs @@ -97,7 +97,7 @@ namespace Avalonia.Input } private bool PenDown(Pointer pointer, ulong timestamp, - IInputElement root, Point p, PointerPointProperties properties, + IInputRoot root, Point p, PointerPointProperties properties, KeyModifiers inputModifiers, IInputElement? hitTest) { var source = pointer.Captured ?? hitTest; @@ -105,7 +105,7 @@ namespace Avalonia.Input if (source != null) { pointer.Capture(source); - var settings = ((IInputRoot?)(source as Interactive)?.GetVisualRoot())?.PlatformSettings; + var settings = (source as Interactive)?.GetPlatformSettings(); if (settings is not null) { var doubleClickTime = settings.GetDoubleTapTime(PointerType.Pen).TotalMilliseconds; @@ -123,7 +123,7 @@ namespace Avalonia.Input } _lastMouseDownButton = properties.PointerUpdateKind.GetMouseButton(); - var e = new PointerPressedEventArgs(source, pointer, (Visual)root, p, timestamp, properties, inputModifiers, _clickCount); + var e = new PointerPressedEventArgs(source, pointer, root.RootElement, p, timestamp, properties, inputModifiers, _clickCount); source.RaiseEvent(e); return e.Handled; } @@ -140,7 +140,7 @@ namespace Avalonia.Input if (source is not null) { - var e = new PointerEventArgs(InputElement.PointerMovedEvent, source, pointer, (Visual)root, + var e = new PointerEventArgs(InputElement.PointerMovedEvent, source, pointer, root.RootElement, p, timestamp, properties, inputModifiers, intermediatePoints); if (pointer.CapturedGestureRecognizer is GestureRecognizer gestureRecognizer) @@ -154,14 +154,14 @@ namespace Avalonia.Input } private bool PenUp(Pointer pointer, ulong timestamp, - IInputElement root, Point p, PointerPointProperties properties, + IInputRoot root, Point p, PointerPointProperties properties, KeyModifiers inputModifiers, IInputElement? hitTest) { var source = pointer.CapturedGestureRecognizer?.Target ?? pointer.Captured ?? hitTest; - + if (source is not null) { - var e = new PointerReleasedEventArgs(source, pointer, (Visual)root, p, timestamp, properties, inputModifiers, + var e = new PointerReleasedEventArgs(source, pointer, root.RootElement, p, timestamp, properties, inputModifiers, _lastMouseDownButton); try diff --git a/src/Avalonia.Base/Input/PinchEventArgs.cs b/src/Avalonia.Base/Input/PinchEventArgs.cs index be373f443a..4c037f30d7 100644 --- a/src/Avalonia.Base/Input/PinchEventArgs.cs +++ b/src/Avalonia.Base/Input/PinchEventArgs.cs @@ -4,13 +4,13 @@ namespace Avalonia.Input { public class PinchEventArgs : RoutedEventArgs { - public PinchEventArgs(double scale, Point scaleOrigin) : base(Gestures.PinchEvent) + public PinchEventArgs(double scale, Point scaleOrigin) : base(InputElement.PinchEvent) { Scale = scale; ScaleOrigin = scaleOrigin; } - public PinchEventArgs(double scale, Point scaleOrigin, double angle, double angleDelta) : base(Gestures.PinchEvent) + public PinchEventArgs(double scale, Point scaleOrigin, double angle, double angleDelta) : base(InputElement.PinchEvent) { Scale = scale; ScaleOrigin = scaleOrigin; @@ -41,7 +41,7 @@ namespace Avalonia.Input public class PinchEndedEventArgs : RoutedEventArgs { - public PinchEndedEventArgs() : base(Gestures.PinchEndedEvent) + public PinchEndedEventArgs() : base(InputElement.PinchEndedEvent) { } } diff --git a/src/Avalonia.Base/Input/Pointer.cs b/src/Avalonia.Base/Input/Pointer.cs index ebb98dd0bc..7de3dbe713 100644 --- a/src/Avalonia.Base/Input/Pointer.cs +++ b/src/Avalonia.Base/Input/Pointer.cs @@ -108,14 +108,14 @@ namespace Avalonia.Input } } - static IInputElement? GetNextCapture(Visual parent) + static IInputElement? GetNextCapture(Visual? parent) { return parent as IInputElement ?? parent.FindAncestorOfType(); } private void OnCaptureDetached(object? sender, VisualTreeAttachmentEventArgs e) { - Capture(GetNextCapture(e.Parent)); + Capture(GetNextCapture(e.AttachmentPoint)); } diff --git a/src/Avalonia.Base/Input/PointerDeltaEventArgs.cs b/src/Avalonia.Base/Input/PointerDeltaEventArgs.cs index 3c4562edf4..6a6c61d315 100644 --- a/src/Avalonia.Base/Input/PointerDeltaEventArgs.cs +++ b/src/Avalonia.Base/Input/PointerDeltaEventArgs.cs @@ -1,7 +1,4 @@ -using System; using Avalonia.Interactivity; -using Avalonia.Metadata; -using Avalonia.VisualTree; namespace Avalonia.Input { @@ -9,8 +6,7 @@ namespace Avalonia.Input { public Vector Delta { get; } - [Unstable("This constructor might be removed in 12.0.")] - public PointerDeltaEventArgs(RoutedEvent routedEvent, object? source, + public PointerDeltaEventArgs(RoutedEvent? routedEvent, object? source, IPointer pointer, Visual rootVisual, Point rootVisualPosition, ulong timestamp, PointerPointProperties properties, KeyModifiers modifiers, Vector delta) : base(routedEvent, source, pointer, rootVisual, rootVisualPosition, diff --git a/src/Avalonia.Base/Input/PointerEventArgs.cs b/src/Avalonia.Base/Input/PointerEventArgs.cs index f4bf785c56..9285e65fa5 100644 --- a/src/Avalonia.Base/Input/PointerEventArgs.cs +++ b/src/Avalonia.Base/Input/PointerEventArgs.cs @@ -14,8 +14,7 @@ namespace Avalonia.Input private readonly PointerPointProperties _properties; private readonly Lazy?>? _previousPoints; - [Unstable("This constructor might be removed in 12.0. For unit testing, consider using IHeadlessWindow mouse methods.")] - public PointerEventArgs(RoutedEvent routedEvent, + public PointerEventArgs(RoutedEvent? routedEvent, object? source, IPointer pointer, Visual? rootVisual, Point rootVisualPosition, @@ -32,8 +31,9 @@ namespace Avalonia.Input Timestamp = timestamp; KeyModifiers = modifiers; } - - internal PointerEventArgs(RoutedEvent routedEvent, + + [PrivateApi] + public PointerEventArgs(RoutedEvent? routedEvent, object? source, IPointer pointer, Visual? rootVisual, Point rootVisualPosition, @@ -41,9 +41,7 @@ namespace Avalonia.Input PointerPointProperties properties, KeyModifiers modifiers, Lazy?>? previousPoints) -#pragma warning disable CS0618 : this(routedEvent, source, pointer, rootVisual, rootVisualPosition, timestamp, properties, modifiers) -#pragma warning restore CS0618 { _previousPoints = previousPoints; } @@ -160,9 +158,8 @@ namespace Avalonia.Input public class PointerPressedEventArgs : PointerEventArgs { - [Unstable("This constructor might be removed in 12.0. For unit testing, consider using IHeadlessWindow mouse methods.")] public PointerPressedEventArgs( - object source, + object? source, IPointer pointer, Visual rootVisual, Point rootVisualPosition, ulong timestamp, @@ -180,9 +177,8 @@ namespace Avalonia.Input public class PointerReleasedEventArgs : PointerEventArgs { - [Unstable("This constructor might be removed in 12.0. For unit testing, consider using IHeadlessWindow mouse methods.")] public PointerReleasedEventArgs( - object source, IPointer pointer, + object? source, IPointer pointer, Visual rootVisual, Point rootVisualPosition, ulong timestamp, PointerPointProperties properties, KeyModifiers modifiers, MouseButton initialPressMouseButton) @@ -202,8 +198,7 @@ namespace Avalonia.Input { public IPointer Pointer { get; } - [Unstable("This constructor might be removed in 12.0. If you need to remove capture, use stable methods on the IPointer instance.")] - public PointerCaptureLostEventArgs(object source, IPointer pointer) : base(InputElement.PointerCaptureLostEvent) + public PointerCaptureLostEventArgs(object? source, IPointer pointer) : base(InputElement.PointerCaptureLostEvent) { Pointer = pointer; Source = source; @@ -216,7 +211,7 @@ namespace Avalonia.Input public CaptureSource CaptureSource { get; } public IInputElement? NewValue { get; } - internal PointerCaptureChangingEventArgs(object source, IPointer pointer, IInputElement? newValue, CaptureSource captureSource) : base(InputElement.PointerCaptureChangingEvent) + internal PointerCaptureChangingEventArgs(object? source, IPointer pointer, IInputElement? newValue, CaptureSource captureSource) : base(InputElement.PointerCaptureChangingEvent) { Pointer = pointer; Source = source; diff --git a/src/Avalonia.Base/Input/PointerOverPreProcessor.cs b/src/Avalonia.Base/Input/PointerOverPreProcessor.cs index ed2b4c8669..beb171467d 100644 --- a/src/Avalonia.Base/Input/PointerOverPreProcessor.cs +++ b/src/Avalonia.Base/Input/PointerOverPreProcessor.cs @@ -38,7 +38,7 @@ namespace Avalonia.Input // occurred. // // Solve this by updating the last known pointer position when a drag event occurs. - _lastKnownPosition = ((Visual)_inputRoot).PointToScreen(dragArgs.Location); + _lastKnownPosition = _inputRoot.RootElement.PointToScreen(dragArgs.Location); } else if (value is RawPointerEventArgs args @@ -64,7 +64,7 @@ namespace Avalonia.Input args.InputModifiers.ToKeyModifiers()); } } - else if (args.Type is RawPointerEventType.TouchBegin or RawPointerEventType.TouchUpdate && args.Root is Visual visual) + else if (args.Type is RawPointerEventType.TouchBegin or RawPointerEventType.TouchUpdate && args.Root.RootElement is {} visual) { _lastKnownPosition = visual.PointToScreen(args.Position); } @@ -99,12 +99,12 @@ namespace Avalonia.Input if (dirtyRect.Contains(clientPoint)) { var element = GetEffectivePointerOverElement( - _inputRoot.InputHitTest(clientPoint), + _inputRoot.RootElement.InputHitTest(clientPoint), pointer.Captured); SetPointerOver(pointer, _inputRoot, element, 0, clientPoint, PointerPointProperties.None, KeyModifiers.None); } - else if (!((Visual)_inputRoot).Bounds.Contains(clientPoint)) + else if (!_inputRoot.RootElement.Bounds.Contains(clientPoint)) { ClearPointerOver(pointer, _inputRoot, 0, clientPoint, PointerPointProperties.None, KeyModifiers.None); } @@ -140,16 +140,16 @@ namespace Avalonia.Input // so GetPosition won't return invalid values. #pragma warning disable CS0618 var e = new PointerEventArgs(InputElement.PointerExitedEvent, element, pointer, - position.HasValue ? root as Visual : null, position.HasValue ? position.Value : default, + position.HasValue ? root.RootElement : null, position.HasValue ? position.Value : default, timestamp, properties, inputModifiers); #pragma warning restore CS0618 if (element is Visual v && !v.IsAttachedToVisualTree) { // element has been removed from visual tree so do top down cleanup - if (root.IsPointerOver) + if (root.RootElement.IsPointerOver) { - ClearChildrenPointerOver(e, root, true); + ClearChildrenPointerOver(e, root.RootElement, true); } } while (element != null) @@ -191,7 +191,7 @@ namespace Avalonia.Input ulong timestamp, Point position, PointerPointProperties properties, KeyModifiers inputModifiers) { var pointerOverElement = root.PointerOverElement; - var screenPosition = ((Visual)root).PointToScreen(position); + var screenPosition = (root.RootElement).PointToScreen(position); _lastKnownPosition = screenPosition; if (element != pointerOverElement) @@ -229,7 +229,7 @@ namespace Avalonia.Input el = root.PointerOverElement; #pragma warning disable CS0618 - var e = new PointerEventArgs(InputElement.PointerExitedEvent, el, pointer, (Visual)root, position, + var e = new PointerEventArgs(InputElement.PointerExitedEvent, el, pointer, root.RootElement, position, timestamp, properties, inputModifiers); #pragma warning restore CS0618 if (el is Visual v && branch != null && !v.IsAttachedToVisualTree) @@ -265,7 +265,7 @@ namespace Avalonia.Input private static Point PointToClient(IInputRoot root, PixelPoint p) { - return ((Visual)root).PointToClient(p); + return (root.RootElement).PointToClient(p); } } } diff --git a/src/Avalonia.Base/Input/PointerWheelEventArgs.cs b/src/Avalonia.Base/Input/PointerWheelEventArgs.cs index 22624a61dd..9cfeaeea74 100644 --- a/src/Avalonia.Base/Input/PointerWheelEventArgs.cs +++ b/src/Avalonia.Base/Input/PointerWheelEventArgs.cs @@ -1,16 +1,10 @@ -using System; -using Avalonia.Interactivity; -using Avalonia.Metadata; -using Avalonia.VisualTree; - namespace Avalonia.Input { public class PointerWheelEventArgs : PointerEventArgs { public Vector Delta { get; } - [Unstable("This constructor might be removed in 12.0. For unit testing, consider using IHeadlessWindow.MouseWheel.")] - public PointerWheelEventArgs(object source, IPointer pointer, Visual rootVisual, + public PointerWheelEventArgs(object? source, IPointer pointer, Visual rootVisual, Point rootVisualPosition, ulong timestamp, PointerPointProperties properties, KeyModifiers modifiers, Vector delta) : base(InputElement.PointerWheelChangedEvent, source, pointer, rootVisual, rootVisualPosition, diff --git a/src/Avalonia.Base/Input/PullGestureEventArgs.cs b/src/Avalonia.Base/Input/PullGestureEventArgs.cs index 34d95c87f4..14b78f8730 100644 --- a/src/Avalonia.Base/Input/PullGestureEventArgs.cs +++ b/src/Avalonia.Base/Input/PullGestureEventArgs.cs @@ -1,4 +1,3 @@ -using System; using Avalonia.Interactivity; namespace Avalonia.Input @@ -12,8 +11,8 @@ namespace Avalonia.Input private static int _nextId = 1; internal static int GetNextFreeId() => _nextId++; - - public PullGestureEventArgs(int id, Vector delta, PullDirection pullDirection) : base(Gestures.PullGestureEvent) + + public PullGestureEventArgs(int id, Vector delta, PullDirection pullDirection) : base(InputElement.PullGestureEvent) { Id = id; Delta = delta; @@ -26,7 +25,7 @@ namespace Avalonia.Input public int Id { get; } public PullDirection PullDirection { get; } - public PullGestureEndedEventArgs(int id, PullDirection pullDirection) : base(Gestures.PullGestureEndedEvent) + public PullGestureEndedEventArgs(int id, PullDirection pullDirection) : base(InputElement.PullGestureEndedEvent) { Id = id; PullDirection = pullDirection; diff --git a/src/Avalonia.Base/Input/ScrollGestureEventArgs.cs b/src/Avalonia.Base/Input/ScrollGestureEventArgs.cs index ac770fd0a2..150ac17226 100644 --- a/src/Avalonia.Base/Input/ScrollGestureEventArgs.cs +++ b/src/Avalonia.Base/Input/ScrollGestureEventArgs.cs @@ -14,7 +14,7 @@ namespace Avalonia.Input public static int GetNextFreeId() => _nextId++; - public ScrollGestureEventArgs(int id, Vector delta) : base(Gestures.ScrollGestureEvent) + public ScrollGestureEventArgs(int id, Vector delta) : base(InputElement.ScrollGestureEvent) { Id = id; Delta = delta; @@ -25,7 +25,7 @@ namespace Avalonia.Input { public int Id { get; } - public ScrollGestureEndedEventArgs(int id) : base(Gestures.ScrollGestureEndedEvent) + public ScrollGestureEndedEventArgs(int id) : base(InputElement.ScrollGestureEndedEvent) { Id = id; } @@ -36,7 +36,7 @@ namespace Avalonia.Input public int Id { get; } public Vector Inertia { get; } - internal ScrollGestureInertiaStartingEventArgs(int id, Vector inertia) : base(Gestures.ScrollGestureInertiaStartingEvent) + internal ScrollGestureInertiaStartingEventArgs(int id, Vector inertia) : base(InputElement.ScrollGestureInertiaStartingEvent) { Id = id; Inertia = inertia; diff --git a/src/Avalonia.Base/Input/SwipeDirection.cs b/src/Avalonia.Base/Input/SwipeDirection.cs new file mode 100644 index 0000000000..3043b443e6 --- /dev/null +++ b/src/Avalonia.Base/Input/SwipeDirection.cs @@ -0,0 +1,28 @@ +namespace Avalonia.Input +{ + /// + /// Specifies the direction of a swipe gesture. + /// + public enum SwipeDirection + { + /// + /// The swipe moved to the left. + /// + Left, + + /// + /// The swipe moved to the right. + /// + Right, + + /// + /// The swipe moved upward. + /// + Up, + + /// + /// The swipe moved downward. + /// + Down + } +} diff --git a/src/Avalonia.Base/Input/SwipeGestureEventArgs.cs b/src/Avalonia.Base/Input/SwipeGestureEventArgs.cs new file mode 100644 index 0000000000..3fa9aede82 --- /dev/null +++ b/src/Avalonia.Base/Input/SwipeGestureEventArgs.cs @@ -0,0 +1,81 @@ +using System; +using System.Threading; +using Avalonia.Interactivity; + +namespace Avalonia.Input +{ + /// + /// Provides data for swipe gesture events. + /// + public class SwipeGestureEventArgs : RoutedEventArgs + { + /// + /// Initializes a new instance of the class. + /// + /// The unique identifier for this gesture. + /// The pixel delta since the last event. + /// The current swipe velocity in pixels per second. + public SwipeGestureEventArgs(int id, Vector delta, Vector velocity) + : base(InputElement.SwipeGestureEvent) + { + Id = id; + Delta = delta; + Velocity = velocity; + SwipeDirection = Math.Abs(delta.X) >= Math.Abs(delta.Y) + ? (delta.X <= 0 ? SwipeDirection.Right : SwipeDirection.Left) + : (delta.Y <= 0 ? SwipeDirection.Down : SwipeDirection.Up); + } + + /// + /// Gets the unique identifier for this gesture sequence. + /// + public int Id { get; } + + /// + /// Gets the pixel delta since the last event. + /// + public Vector Delta { get; } + + /// + /// Gets the current swipe velocity in pixels per second. + /// + public Vector Velocity { get; } + + /// + /// Gets the direction of the dominant swipe axis. + /// + public SwipeDirection SwipeDirection { get; } + + private static int s_nextId; + + internal static int GetNextFreeId() => Interlocked.Increment(ref s_nextId); + } + + /// + /// Provides data for the swipe gesture ended event. + /// + public class SwipeGestureEndedEventArgs : RoutedEventArgs + { + /// + /// Initializes a new instance of the class. + /// + /// The unique identifier for this gesture. + /// The swipe velocity at release in pixels per second. + public SwipeGestureEndedEventArgs(int id, Vector velocity) + : base(InputElement.SwipeGestureEndedEvent) + { + Id = id; + Velocity = velocity; + } + + /// + /// Gets the unique identifier for this gesture sequence. + /// + public int Id { get; } + + /// + /// Gets the swipe velocity at release in pixels per second. + /// + public Vector Velocity { get; } + } +} diff --git a/src/Avalonia.Base/Input/TappedEventArgs.cs b/src/Avalonia.Base/Input/TappedEventArgs.cs index eaffa1d8bc..924f4be11f 100644 --- a/src/Avalonia.Base/Input/TappedEventArgs.cs +++ b/src/Avalonia.Base/Input/TappedEventArgs.cs @@ -1,6 +1,4 @@ -using System; using Avalonia.Interactivity; -using Avalonia.VisualTree; namespace Avalonia.Input { @@ -8,7 +6,7 @@ namespace Avalonia.Input { private readonly PointerEventArgs lastPointerEventArgs; - public TappedEventArgs(RoutedEvent routedEvent, PointerEventArgs lastPointerEventArgs) + public TappedEventArgs(RoutedEvent? routedEvent, PointerEventArgs lastPointerEventArgs) : base(routedEvent) { this.lastPointerEventArgs = lastPointerEventArgs; @@ -17,7 +15,7 @@ namespace Avalonia.Input public IPointer Pointer => lastPointerEventArgs.Pointer; public KeyModifiers KeyModifiers => lastPointerEventArgs.KeyModifiers; public ulong Timestamp => lastPointerEventArgs.Timestamp; - + public Point GetPosition(Visual? relativeTo) => lastPointerEventArgs.GetPosition(relativeTo); } } diff --git a/src/Avalonia.Base/Input/TextInput/ITextInputMethodImpl.cs b/src/Avalonia.Base/Input/TextInput/ITextInputMethodImpl.cs index 2969b2e60d..471bbaa081 100644 --- a/src/Avalonia.Base/Input/TextInput/ITextInputMethodImpl.cs +++ b/src/Avalonia.Base/Input/TextInput/ITextInputMethodImpl.cs @@ -10,10 +10,4 @@ namespace Avalonia.Input.TextInput void SetOptions(TextInputOptions options); void Reset(); } - - [NotClientImplementable] - public interface ITextInputMethodRoot : IInputRoot - { - ITextInputMethodImpl? InputMethod { get; } - } } diff --git a/src/Avalonia.Base/Input/TextInput/InputMethodManager.cs b/src/Avalonia.Base/Input/TextInput/InputMethodManager.cs index 005a015644..930e827874 100644 --- a/src/Avalonia.Base/Input/TextInput/InputMethodManager.cs +++ b/src/Avalonia.Base/Input/TextInput/InputMethodManager.cs @@ -132,7 +132,7 @@ namespace Avalonia.Input.TextInput InputMethod.AddTextInputMethodClientRequeryRequestedHandler(_visualRoot, TextInputMethodClientRequeryRequested); - var inputMethod = ((element as Visual)?.VisualRoot as ITextInputMethodRoot)?.InputMethod; + var inputMethod = ((element as Visual)?.GetInputRoot())?.InputMethod; if (_im != inputMethod) { diff --git a/src/Avalonia.Base/Input/TouchDevice.cs b/src/Avalonia.Base/Input/TouchDevice.cs index 8e662ea1b9..27381bbe00 100644 --- a/src/Avalonia.Base/Input/TouchDevice.cs +++ b/src/Avalonia.Base/Input/TouchDevice.cs @@ -51,7 +51,7 @@ namespace Avalonia.Input pointer.Capture(hit); } - var target = pointer.Captured ?? args.InputHitTestResult.firstEnabledAncestor ?? args.Root; + var target = pointer.Captured ?? args.InputHitTestResult.firstEnabledAncestor ?? args.Root.RootElement; var gestureTarget = pointer.CapturedGestureRecognizer?.Target; var updateKind = args.Type.ToUpdateKind(); var keyModifier = args.InputModifiers.ToKeyModifiers(); @@ -66,7 +66,7 @@ namespace Avalonia.Input } else { - var settings = ((IInputRoot?)(target as Interactive)?.GetVisualRoot())?.PlatformSettings; + var settings = (target as Interactive)?.GetPlatformSettings(); if (settings is not null) { var doubleClickTime = settings.GetDoubleTapTime(PointerType.Touch).TotalMilliseconds; @@ -86,7 +86,7 @@ namespace Avalonia.Input } target.RaiseEvent(new PointerPressedEventArgs(target, pointer, - (Visual)args.Root, args.Position, ev.Timestamp, + args.Root.RootElement, args.Position, ev.Timestamp, new PointerPointProperties(GetModifiers(args.InputModifiers, true), updateKind, args.Point), keyModifier, _clickCount)); } @@ -98,7 +98,7 @@ namespace Avalonia.Input { target = gestureTarget ?? target; var e = new PointerReleasedEventArgs(target, pointer, - (Visual)args.Root, args.Position, ev.Timestamp, + args.Root.RootElement, args.Position, ev.Timestamp, new PointerPointProperties(GetModifiers(args.InputModifiers, false), updateKind, args.Point), keyModifier, MouseButton.Left); if (gestureTarget != null) @@ -127,7 +127,7 @@ namespace Avalonia.Input if (args.Type == RawPointerEventType.TouchUpdate) { target = gestureTarget ?? target; - var e = new PointerEventArgs(InputElement.PointerMovedEvent, target, pointer!, (Visual)args.Root, + var e = new PointerEventArgs(InputElement.PointerMovedEvent, target, pointer!, args.Root.RootElement, args.Position, ev.Timestamp, new PointerPointProperties(GetModifiers(args.InputModifiers, true), updateKind, args.Point), keyModifier, args.IntermediatePoints); diff --git a/src/Avalonia.Base/Input/WindowDecorationsElementRole.cs b/src/Avalonia.Base/Input/WindowDecorationsElementRole.cs new file mode 100644 index 0000000000..faa0922c33 --- /dev/null +++ b/src/Avalonia.Base/Input/WindowDecorationsElementRole.cs @@ -0,0 +1,100 @@ +namespace Avalonia.Input; + +/// +/// Defines the cross-platform role of a visual element for non-client hit-testing. +/// Used to mark elements as titlebar drag areas, resize grips, etc. +/// +public enum WindowDecorationsElementRole +{ + /// + /// No special role. The element is invisible to chrome hit-testing. + /// + None, + + /// + /// An interactive element that is part of the decorations chrome (e.g., a caption button). + /// Set by themes on decoration template elements. Input is passed through to the element + /// rather than being intercepted for non-client actions. + /// + DecorationsElement, + + /// + /// An interactive element set by user code that should receive input even when + /// overlapping chrome areas. Has the same effect as + /// but is intended for use by application developers. + /// + User, + + /// + /// The element acts as a titlebar drag area. + /// Clicking and dragging on this element initiates a platform window move. + /// + TitleBar, + + /// + /// Resize grip for the north (top) edge. + /// + ResizeN, + + /// + /// Resize grip for the south (bottom) edge. + /// + ResizeS, + + /// + /// Resize grip for the east (right) edge. + /// + ResizeE, + + /// + /// Resize grip for the west (left) edge. + /// + ResizeW, + + /// + /// Resize grip for the northeast corner. + /// + ResizeNE, + + /// + /// Resize grip for the northwest corner. + /// + ResizeNW, + + /// + /// Resize grip for the southeast corner. + /// + ResizeSE, + + /// + /// Resize grip for the southwest corner. + /// + ResizeSW, + + /// + /// The element acts as the window close button. + /// On Win32, maps to HTCLOSE for system close behavior. + /// On other platforms, treated as an interactive decoration element. + /// + CloseButton, + + /// + /// The element acts as the window minimize button. + /// On Win32, maps to HTMINBUTTON for system minimize behavior. + /// On other platforms, treated as an interactive decoration element. + /// + MinimizeButton, + + /// + /// The element acts as the window maximize/restore button. + /// On Win32, maps to HTMAXBUTTON for system maximize behavior. + /// On other platforms, treated as an interactive decoration element. + /// + MaximizeButton, + + /// + /// The element acts as the window fullscreen toggle button. + /// Treated as an interactive decoration element on all platforms. + /// + FullScreenButton +} diff --git a/src/Avalonia.Base/Layout/IEmbeddedLayoutRoot.cs b/src/Avalonia.Base/Layout/IEmbeddedLayoutRoot.cs deleted file mode 100644 index 24f0ccd82e..0000000000 --- a/src/Avalonia.Base/Layout/IEmbeddedLayoutRoot.cs +++ /dev/null @@ -1,10 +0,0 @@ -namespace Avalonia.Layout -{ - /// - /// A special layout root with enforced size for Arrange pass - /// - public interface IEmbeddedLayoutRoot : ILayoutRoot - { - Size AllocatedSize { get; } - } -} \ No newline at end of file diff --git a/src/Avalonia.Base/Layout/ILayoutManager.cs b/src/Avalonia.Base/Layout/ILayoutManager.cs index 5035d3a48d..560d0aab00 100644 --- a/src/Avalonia.Base/Layout/ILayoutManager.cs +++ b/src/Avalonia.Base/Layout/ILayoutManager.cs @@ -6,7 +6,6 @@ namespace Avalonia.Layout /// /// Manages measuring and arranging of controls. /// - [PrivateApi] public interface ILayoutManager : IDisposable { /// diff --git a/src/Avalonia.Base/Layout/ILayoutRoot.cs b/src/Avalonia.Base/Layout/ILayoutRoot.cs index efea5bfed8..8624194c42 100644 --- a/src/Avalonia.Base/Layout/ILayoutRoot.cs +++ b/src/Avalonia.Base/Layout/ILayoutRoot.cs @@ -5,22 +5,18 @@ namespace Avalonia.Layout /// /// Defines the root of a layoutable tree. /// - [NotClientImplementable] - public interface ILayoutRoot + internal interface ILayoutRoot { - /// - /// The size available to lay out the controls. - /// - Size ClientSize { get; } - /// /// The scaling factor to use in layout. /// - double LayoutScaling { get; } + public double LayoutScaling { get; } /// /// Associated instance of layout manager /// - internal ILayoutManager LayoutManager { get; } + public ILayoutManager LayoutManager { get; } + + public Layoutable RootVisual { get; } } } diff --git a/src/Avalonia.Base/Layout/LayoutHelper.cs b/src/Avalonia.Base/Layout/LayoutHelper.cs index a342c654f9..fd81ffaa49 100644 --- a/src/Avalonia.Base/Layout/LayoutHelper.cs +++ b/src/Avalonia.Base/Layout/LayoutHelper.cs @@ -140,16 +140,14 @@ namespace Avalonia.Layout /// /// The control. /// Thrown when control has no root or returned layout scaling is invalid. - public static double GetLayoutScale(Layoutable control) - => control.VisualRoot is ILayoutRoot layoutRoot ? layoutRoot.LayoutScaling : 1.0; + public static double GetLayoutScale(Layoutable control) => control.GetLayoutRoot()?.LayoutScaling ?? 1.0; /// /// Rounds a size to integer values for layout purposes, compensating for high DPI screen /// coordinates by rounding the size up to the nearest pixel. /// /// Input size. - /// DPI along x-dimension. - /// DPI along y-dimension. + /// The DPI scale. /// Value of size that will be rounded under screen DPI. /// /// This is a layout helper method. It takes DPI into account and also does not return @@ -157,14 +155,8 @@ namespace Avalonia.Layout /// associated with the UseLayoutRounding property and should not be used as a general rounding /// utility. /// - public static Size RoundLayoutSizeUp(Size size, double dpiScaleX, double dpiScaleY) - { - return new Size(RoundLayoutValueUp(size.Width, dpiScaleX), RoundLayoutValueUp(size.Height, dpiScaleY)); - } - [SuppressMessage("ReSharper", "CompareOfFloatsByEqualityOperator", Justification = "The DPI scale should have been normalized.")] - [MethodImpl(MethodImplOptions.AggressiveInlining)] - internal static Size RoundLayoutSizeUp(Size size, double dpiScale) + public static Size RoundLayoutSizeUp(Size size, double dpiScale) { // If DPI == 1, don't use DPI-aware rounding. return dpiScale == 1.0 ? @@ -181,8 +173,7 @@ namespace Avalonia.Layout /// coordinates. /// /// Input thickness. - /// DPI along x-dimension. - /// DPI along y-dimension. + /// The DPI scale. /// Value of thickness that will be rounded under screen DPI. /// /// This is a layout helper method. It takes DPI into account and also does not return @@ -190,19 +181,8 @@ namespace Avalonia.Layout /// associated with the UseLayoutRounding property and should not be used as a general rounding /// utility. /// - public static Thickness RoundLayoutThickness(Thickness thickness, double dpiScaleX, double dpiScaleY) - { - return new Thickness( - RoundLayoutValue(thickness.Left, dpiScaleX), - RoundLayoutValue(thickness.Top, dpiScaleY), - RoundLayoutValue(thickness.Right, dpiScaleX), - RoundLayoutValue(thickness.Bottom, dpiScaleY) - ); - } - [SuppressMessage("ReSharper", "CompareOfFloatsByEqualityOperator", Justification = "The DPI scale should have been normalized.")] - [MethodImpl(MethodImplOptions.AggressiveInlining)] - internal static Thickness RoundLayoutThickness(Thickness thickness, double dpiScale) + public static Thickness RoundLayoutThickness(Thickness thickness, double dpiScale) { // If DPI == 1, don't use DPI-aware rounding. return dpiScale == 1.0 ? @@ -220,7 +200,7 @@ namespace Avalonia.Layout [SuppressMessage("ReSharper", "CompareOfFloatsByEqualityOperator", Justification = "The DPI scale should have been normalized.")] [MethodImpl(MethodImplOptions.AggressiveInlining)] - internal static Point RoundLayoutPoint(Point point, double dpiScale) + public static Point RoundLayoutPoint(Point point, double dpiScale) { // If DPI == 1, don't use DPI-aware rounding. return dpiScale == 1.0 ? @@ -245,10 +225,11 @@ namespace Avalonia.Layout /// associated with the UseLayoutRounding property and should not be used as a general rounding /// utility. /// + [SuppressMessage("ReSharper", "CompareOfFloatsByEqualityOperator", Justification = "The DPI scale should have been normalized.")] public static double RoundLayoutValue(double value, double dpiScale) { // If DPI == 1, don't use DPI-aware rounding. - return MathUtilities.IsOne(dpiScale) ? + return dpiScale == 1.0 ? Math.Round(value) : Math.Round(value * dpiScale) / dpiScale; } @@ -266,10 +247,11 @@ namespace Avalonia.Layout /// associated with the UseLayoutRounding property and should not be used as a general rounding /// utility. /// + [SuppressMessage("ReSharper", "CompareOfFloatsByEqualityOperator", Justification = "The DPI scale should have been normalized.")] public static double RoundLayoutValueUp(double value, double dpiScale) { // If DPI == 1, don't use DPI-aware rounding. - return MathUtilities.IsOne(dpiScale) ? + return dpiScale == 1.0 ? Math.Ceiling(value) : Math.Ceiling(RoundTo8Digits(value) * dpiScale) / dpiScale; } @@ -281,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 } } } diff --git a/src/Avalonia.Base/Layout/LayoutManager.cs b/src/Avalonia.Base/Layout/LayoutManager.cs index 411b747107..fa82ff1d31 100644 --- a/src/Avalonia.Base/Layout/LayoutManager.cs +++ b/src/Avalonia.Base/Layout/LayoutManager.cs @@ -9,6 +9,7 @@ using Avalonia.Metadata; using Avalonia.Rendering; using Avalonia.Threading; using Avalonia.Utilities; +using Avalonia.VisualTree; #nullable enable @@ -17,11 +18,10 @@ namespace Avalonia.Layout /// /// Manages measuring and arranging of controls. /// - [PrivateApi] - public class LayoutManager : ILayoutManager, IDisposable + internal class LayoutManager : ILayoutManager, IDisposable { private const int MaxPasses = 10; - private readonly Layoutable _owner; + private readonly ILayoutRoot _owner; private readonly LayoutQueue _toMeasure = new LayoutQueue(v => !v.IsMeasureValid); private readonly LayoutQueue _toArrange = new LayoutQueue(v => !v.IsArrangeValid); private readonly List _toArrangeAfterMeasure = new(); @@ -34,7 +34,7 @@ namespace Avalonia.Layout public LayoutManager(ILayoutRoot owner) { - _owner = owner as Layoutable ?? throw new ArgumentNullException(nameof(owner)); + _owner = owner; _invokeOnRender = ExecuteQueuedLayoutPass; } @@ -63,7 +63,7 @@ namespace Avalonia.Layout #endif } - if (control.VisualRoot != _owner) + if (control.GetLayoutRoot() != _owner) { throw new ArgumentException("Attempt to call InvalidateMeasure on wrong LayoutManager."); } @@ -93,7 +93,7 @@ namespace Avalonia.Layout #endif } - if (control.VisualRoot != _owner) + if (control.GetLayoutRoot() != _owner) { throw new ArgumentException("Attempt to call InvalidateArrange on wrong LayoutManager."); } @@ -188,9 +188,12 @@ namespace Avalonia.Layout try { + if (_owner?.RootVisual == null) + return; + var root = _owner.RootVisual; _running = true; - Measure(_owner); - Arrange(_owner); + Measure(root); + Arrange(root); } finally { @@ -300,7 +303,7 @@ namespace Avalonia.Layout // control to be removed. if (!control.IsMeasureValid) { - if (control is ILayoutRoot root) + if (control.GetLayoutRoot()?.RootVisual == control) { control.Measure(Size.Infinity); } @@ -329,9 +332,7 @@ namespace Avalonia.Layout if (!control.IsArrangeValid) { - if (control is IEmbeddedLayoutRoot embeddedRoot) - control.Arrange(new Rect(embeddedRoot.AllocatedSize)); - else if (control is ILayoutRoot root) + if (control.GetLayoutRoot()?.RootVisual == control) control.Arrange(new Rect(control.DesiredSize)); else if (control.PreviousArrange != null) { diff --git a/src/Avalonia.Base/Layout/Layoutable.cs b/src/Avalonia.Base/Layout/Layoutable.cs index 9fa0f7689f..e0c316c60a 100644 --- a/src/Avalonia.Base/Layout/Layoutable.cs +++ b/src/Avalonia.Base/Layout/Layoutable.cs @@ -113,7 +113,7 @@ namespace Avalonia.Layout /// Defines the property. /// public static readonly StyledProperty MarginProperty = - AvaloniaProperty.Register(nameof(Margin)); + AvaloniaProperty.Register(nameof(Margin), validate: ValidateThickness); /// /// Defines the property. @@ -161,6 +161,8 @@ namespace Avalonia.Layout private static bool ValidateMinimumDimension(double value) => !double.IsPositiveInfinity(value) && ValidateMaximumDimension(value); private static bool ValidateMaximumDimension(double value) => value >= 0; + private static bool ValidateThickness(Thickness value) => double.IsFinite(value.Left) && double.IsFinite(value.Top) && double.IsFinite(value.Right) && double.IsFinite(value.Bottom); + /// /// Occurs when the element's effective viewport changes. /// @@ -168,7 +170,7 @@ namespace Avalonia.Layout { add { - if (_effectiveViewportChanged is null && VisualRoot is ILayoutRoot r && !_isAttachingToVisualTree) + if (_effectiveViewportChanged is null && this.GetLayoutRoot() is {} r && !_isAttachingToVisualTree) { r.LayoutManager.RegisterEffectiveViewportListener(this); } @@ -180,7 +182,7 @@ namespace Avalonia.Layout { _effectiveViewportChanged -= value; - if (_effectiveViewportChanged is null && VisualRoot is ILayoutRoot r) + if (_effectiveViewportChanged is null && this.GetLayoutRoot() is {} r) { r.LayoutManager.UnregisterEffectiveViewportListener(this); } @@ -194,7 +196,7 @@ namespace Avalonia.Layout { add { - if (_layoutUpdated is null && VisualRoot is ILayoutRoot r && !_isAttachingToVisualTree) + if (_layoutUpdated is null && this.GetLayoutRoot() is {} r && !_isAttachingToVisualTree) { r.LayoutManager.LayoutUpdated += LayoutManagedLayoutUpdated; } @@ -206,7 +208,7 @@ namespace Avalonia.Layout { _layoutUpdated -= value; - if (_layoutUpdated is null && VisualRoot is ILayoutRoot r) + if (_layoutUpdated is null && this.GetLayoutRoot() is {} r) { r.LayoutManager.LayoutUpdated -= LayoutManagedLayoutUpdated; } @@ -220,7 +222,8 @@ namespace Avalonia.Layout /// You should not usually need to call this method explictly, the layout manager will /// schedule layout passes itself. /// - public void UpdateLayout() => (this.GetVisualRoot() as ILayoutRoot)?.LayoutManager?.ExecuteLayoutPass(); + + public void UpdateLayout() => this.GetLayoutManager()?.ExecuteLayoutPass(); /// /// Gets or sets the width of the element. @@ -448,7 +451,7 @@ namespace Avalonia.Layout if (IsAttachedToVisualTree) { - (VisualRoot as ILayoutRoot)?.LayoutManager.InvalidateMeasure(this); + this.GetLayoutManager()?.InvalidateMeasure(this); InvalidateVisual(); } OnMeasureInvalidated(); @@ -465,7 +468,7 @@ namespace Avalonia.Layout Logger.TryGet(LogEventLevel.Verbose, LogArea.Layout)?.Log(this, "Invalidated arrange"); IsArrangeValid = false; - (VisualRoot as ILayoutRoot)?.LayoutManager?.InvalidateArrange(this); + this.GetLayoutManager()?.InvalidateArrange(this); InvalidateVisual(); } } @@ -793,7 +796,7 @@ namespace Avalonia.Layout _isAttachingToVisualTree = false; } - if (e.Root is ILayoutRoot r) + if (this.GetLayoutRoot() is {} r) { if (_layoutUpdated is object) { @@ -809,7 +812,7 @@ namespace Avalonia.Layout protected override void OnDetachedFromVisualTreeCore(VisualTreeAttachmentEventArgs e) { - if (e.Root is ILayoutRoot r) + if (this.GetLayoutRoot() is {} r) { if (_layoutUpdated is object) { @@ -852,7 +855,7 @@ namespace Avalonia.Layout // they will need to be registered with the layout manager now that they // are again effectively visible. If IsEffectivelyVisible becomes an observable // property then we can piggy-pack on that; for the moment we do this manually. - if (VisualRoot is ILayoutRoot layoutRoot) + if (this.GetLayoutRoot() is {} layoutRoot) { var count = VisualChildren.Count; diff --git a/src/Avalonia.Base/Logging/LogArea.cs b/src/Avalonia.Base/Logging/LogArea.cs index 139129e623..07553a647e 100644 --- a/src/Avalonia.Base/Logging/LogArea.cs +++ b/src/Avalonia.Base/Logging/LogArea.cs @@ -20,6 +20,11 @@ namespace Avalonia.Logging /// public const string Animations = nameof(Animations); + /// + /// The log event comes from the fonts system. + /// + public const string Fonts = nameof(Fonts); + /// /// The log event comes from the visual system. /// diff --git a/src/Avalonia.Base/Media/FontManager.cs b/src/Avalonia.Base/Media/FontManager.cs index 9d1d0145d0..1f15820b9a 100644 --- a/src/Avalonia.Base/Media/FontManager.cs +++ b/src/Avalonia.Base/Media/FontManager.cs @@ -199,7 +199,7 @@ namespace Avalonia.Media return true; } - var logger = Logger.TryGet(LogEventLevel.Debug, "FontManager"); + var logger = Logger.TryGet(LogEventLevel.Debug, LogArea.Fonts); logger?.Log(this, $"Font family '{familyName}' could not be found. Present font families: [{string.Join(",", fontCollection)}]"); diff --git a/src/Avalonia.Base/Media/Fonts/FamilyNameCollection.cs b/src/Avalonia.Base/Media/Fonts/FamilyNameCollection.cs index dabe935b76..ed916bb441 100644 --- a/src/Avalonia.Base/Media/Fonts/FamilyNameCollection.cs +++ b/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 /// /// Gets the primary family name. diff --git a/src/Avalonia.Base/Media/Fonts/FontCollectionBase.cs b/src/Avalonia.Base/Media/Fonts/FontCollectionBase.cs index 4c535bdc0e..6e4283d76e 100644 --- a/src/Avalonia.Base/Media/Fonts/FontCollectionBase.cs +++ b/src/Avalonia.Base/Media/Fonts/FontCollectionBase.cs @@ -5,6 +5,7 @@ using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; using System.Globalization; using System.IO; +using Avalonia.Media.Fonts.Tables; using Avalonia.Platform; namespace Avalonia.Media.Fonts @@ -55,35 +56,40 @@ namespace Avalonia.Media.Fonts } } - //Try to find a match in any font family - foreach (var pair in _glyphTypefaceCache) + return TryMatchInAnyFamily(isLastResort: false, out match) || + TryMatchInAnyFamily(isLastResort: true, out match); + + bool TryMatchInAnyFamily(bool isLastResort, out Typeface match) { - if (pair.Key == familyName) + //Try to find a match in any font family + foreach (var pair in _glyphTypefaceCache) { - //We already tried this before - continue; - } - - glyphTypefaces = pair.Value; + if (pair.Key == familyName) + { + //We already tried this before + continue; + } - if (TryGetNearestMatch(glyphTypefaces, key, out var glyphTypeface)) - { - if (glyphTypeface.CharacterToGlyphMap.TryGetGlyph(codepoint, out _)) + if (TryGetNearestMatchCore(pair.Value, key, isLastResort, out var glyphTypeface)) { - var platformTypeface = glyphTypeface.PlatformTypeface; + if (glyphTypeface.CharacterToGlyphMap.TryGetGlyph(codepoint, out _)) + { + var platformTypeface = glyphTypeface.PlatformTypeface; - // Found a match - match = new Typeface(new FontFamily(null, Key.AbsoluteUri + "#" + glyphTypeface.FamilyName), - platformTypeface.Style, - platformTypeface.Weight, - platformTypeface.Stretch); + // Found a match + match = new Typeface(new FontFamily(null, Key.AbsoluteUri + "#" + glyphTypeface.FamilyName), + platformTypeface.Style, + platformTypeface.Weight, + platformTypeface.Stretch); - return true; + return true; + } } } - } - return false; + match = default; + return false; + } } public virtual bool TryCreateSyntheticGlyphTypeface( @@ -128,7 +134,9 @@ namespace Avalonia.Media.Fonts { if (_fontManagerImpl.TryCreateGlyphTypeface(stream, fontSimulations, out var platformTypeface)) { - syntheticGlyphTypeface = new GlyphTypeface(platformTypeface, fontSimulations); + syntheticGlyphTypeface = GlyphTypeface.TryCreate(platformTypeface, fontSimulations); + if (syntheticGlyphTypeface is null) + return false; //Add the TypographicFamilyName to the cache if (!string.IsNullOrEmpty(glyphTypeface.TypographicFamilyName)) @@ -160,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? familyTypefaces) @@ -272,16 +280,14 @@ namespace Avalonia.Media.Fonts /// langword="false"/>. public bool TryAddGlyphTypeface(Stream stream, [NotNullWhen(true)] out GlyphTypeface? glyphTypeface) { - glyphTypeface = null; - if (!_fontManagerImpl.TryCreateGlyphTypeface(stream, FontSimulations.None, out var platformTypeface)) { + glyphTypeface = null; return false; } - glyphTypeface = new GlyphTypeface(platformTypeface); - - return TryAddGlyphTypeface(glyphTypeface); + glyphTypeface = GlyphTypeface.TryCreate(platformTypeface); + return glyphTypeface is not null && TryAddGlyphTypeface(glyphTypeface); } /// @@ -315,13 +321,12 @@ namespace Avalonia.Media.Fonts { var stream = _assetLoader.Open(fontAsset); - if (!_fontManagerImpl.TryCreateGlyphTypeface(stream, FontSimulations.None, out var platformTypeface)) + if (!_fontManagerImpl.TryCreateGlyphTypeface(stream, FontSimulations.None, out var platformTypeface) || + GlyphTypeface.TryCreate(platformTypeface) is not { } glyphTypeface) { continue; } - var glyphTypeface = new GlyphTypeface(platformTypeface); - var key = glyphTypeface.ToFontCollectionKey(); //Add TypographicFamilyName to the cache @@ -353,14 +358,11 @@ namespace Avalonia.Media.Fonts using var stream = File.OpenRead(source.LocalPath); - if (_fontManagerImpl.TryCreateGlyphTypeface(stream, FontSimulations.None, out var platformTypeface)) + if (_fontManagerImpl.TryCreateGlyphTypeface(stream, FontSimulations.None, out var platformTypeface) && + GlyphTypeface.TryCreate(platformTypeface) is { } glyphTypeface && + TryAddGlyphTypeface(glyphTypeface)) { - var glyphTypeface = new GlyphTypeface(platformTypeface); - - if (TryAddGlyphTypeface(glyphTypeface)) - { - result = true; - } + result = true; } } // If the path is a directory, load all font files from that directory @@ -377,14 +379,11 @@ namespace Avalonia.Media.Fonts { using var stream = File.OpenRead(file); - if (_fontManagerImpl.TryCreateGlyphTypeface(stream, FontSimulations.None, out var platformTypeface)) + if (_fontManagerImpl.TryCreateGlyphTypeface(stream, FontSimulations.None, out var platformTypeface) && + GlyphTypeface.TryCreate(platformTypeface) is { } glyphTypeface && + TryAddGlyphTypeface(glyphTypeface)) { - var glyphTypeface = new GlyphTypeface(platformTypeface); - - if (TryAddGlyphTypeface(glyphTypeface)) - { - result = true; - } + result = true; } } } @@ -456,25 +455,25 @@ namespace Avalonia.Media.Fonts /// find the best match based on the provided . /// The name of the font family to search for. This parameter is case-insensitive. /// The key representing the desired font collection attributes. + /// Whether to allow a nearest match (as opposed to only an exact match). /// When this method returns, contains the matching if a match is found; otherwise, /// . /// if a matching glyph typeface is found; otherwise, . - 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)) { @@ -512,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; } @@ -550,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; } @@ -560,12 +559,35 @@ namespace Avalonia.Media.Fonts return false; } + private bool TryGetMatch( + IDictionary 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; + } + /// /// Attempts to retrieve the nearest matching for the specified font key from the /// provided collection of glyph typefaces. /// /// This method attempts to find the best match for the specified font key by considering - /// various fallback strategies, such as normalizing the font style, stretch, and weight. + /// various fallback strategies, such as normalizing the font style, stretch, and weight. /// If no suitable match is found, the method will return the first available non-null from the /// collection, if any. /// A collection of glyph typefaces, indexed by . @@ -574,10 +596,22 @@ namespace Avalonia.Media.Fonts /// key, if a match is found; otherwise, . /// if a matching is found; otherwise, . - protected bool TryGetNearestMatch(IDictionary glyphTypefaces, + protected bool TryGetNearestMatch(IDictionary glyphTypefaces, FontCollectionKey key, [NotNullWhen(true)] out GlyphTypeface? glyphTypeface) { - if (glyphTypefaces.TryGetValue(key, out glyphTypeface) && glyphTypeface != null) + return TryGetNearestMatchCore(glyphTypefaces, key, isLastResort: false, out glyphTypeface) + || TryGetNearestMatchCore(glyphTypefaces, key, isLastResort: true, out glyphTypeface); + } + + private static bool TryGetNearestMatchCore( + IDictionary glyphTypefaces, + FontCollectionKey key, + bool isLastResort, + [NotNullWhen(true)] out GlyphTypeface? glyphTypeface) + { + if (glyphTypefaces.TryGetValue(key, out glyphTypeface) && + glyphTypeface != null && + glyphTypeface.IsLastResort == isLastResort) { return true; } @@ -589,14 +623,14 @@ namespace Avalonia.Media.Fonts if (key.Stretch != FontStretch.Normal) { - if (TryFindStretchFallback(glyphTypefaces, key, out glyphTypeface)) + if (TryFindStretchFallback(glyphTypefaces, key, isLastResort, out glyphTypeface)) { return true; } if (key.Weight != FontWeight.Normal) { - if (TryFindStretchFallback(glyphTypefaces, key with { Weight = FontWeight.Normal }, out glyphTypeface)) + if (TryFindStretchFallback(glyphTypefaces, key with { Weight = FontWeight.Normal }, isLastResort, out glyphTypeface)) { return true; } @@ -605,12 +639,12 @@ namespace Avalonia.Media.Fonts key = key with { Stretch = FontStretch.Normal }; } - if (TryFindWeightFallback(glyphTypefaces, key, out glyphTypeface)) + if (TryFindWeightFallback(glyphTypefaces, key, isLastResort, out glyphTypeface)) { return true; } - if (TryFindStretchFallback(glyphTypefaces, key, out glyphTypeface)) + if (TryFindStretchFallback(glyphTypefaces, key, isLastResort, out glyphTypeface)) { return true; } @@ -618,7 +652,7 @@ namespace Avalonia.Media.Fonts //Take the first glyph typeface we can find. foreach (var typeface in glyphTypefaces.Values) { - if (typeface != null) + if (typeface != null && isLastResort == typeface.IsLastResort) { glyphTypeface = typeface; @@ -702,12 +736,14 @@ namespace Avalonia.Media.Fonts /// A dictionary mapping font collection keys to their corresponding glyph typefaces. Used as the source for /// searching fallback typefaces. /// The font collection key specifying the desired font stretch and other font attributes to match. + /// Whether to match last resort fonts. /// When this method returns, contains the found glyph typeface with a similar stretch if one exists; otherwise, /// null. /// true if a suitable fallback glyph typeface is found; otherwise, false. private static bool TryFindStretchFallback( IDictionary glyphTypefaces, FontCollectionKey key, + bool isLastResort, [NotNullWhen(true)] out GlyphTypeface? glyphTypeface) { glyphTypeface = null; @@ -718,7 +754,7 @@ namespace Avalonia.Media.Fonts { for (var i = 0; stretch + i < 9; i++) { - if (glyphTypefaces.TryGetValue(key with { Stretch = (FontStretch)(stretch + i) }, out glyphTypeface) && glyphTypeface != null) + if (TryGetWithStretch(stretch + i, out glyphTypeface)) { return true; } @@ -728,13 +764,18 @@ namespace Avalonia.Media.Fonts { for (var i = 0; stretch - i > 1; i++) { - if (glyphTypefaces.TryGetValue(key with { Stretch = (FontStretch)(stretch - i) }, out glyphTypeface) && glyphTypeface != null) + if (TryGetWithStretch(stretch - i, out glyphTypeface)) { return true; } } } + bool TryGetWithStretch(int effectiveStretch, [NotNullWhen(true)] out GlyphTypeface? glyphTypeface) + => glyphTypefaces.TryGetValue(key with { Stretch = (FontStretch)effectiveStretch }, out glyphTypeface) && + glyphTypeface != null && + glyphTypeface.IsLastResort == isLastResort; + return false; } @@ -749,12 +790,14 @@ namespace Avalonia.Media.Fonts /// for a suitable fallback. /// The font collection key specifying the desired font attributes, including weight, for which a fallback glyph /// typeface is sought. + /// Whether to match last resort fonts. /// When this method returns, contains the matching glyph typeface if a suitable fallback is found; otherwise, /// null. /// true if a fallback glyph typeface matching the requested weight is found; otherwise, false. private static bool TryFindWeightFallback( IDictionary glyphTypefaces, FontCollectionKey key, + bool isLastResort, [NotNullWhen(true)] out GlyphTypeface? glyphTypeface) { glyphTypeface = null; @@ -766,7 +809,7 @@ namespace Avalonia.Media.Fonts //Look for available weights between the target and 500, in ascending order. for (var i = 0; weight + i <= 500; i += 50) { - if (glyphTypefaces.TryGetValue(key with { Weight = (FontWeight)(weight + i) }, out glyphTypeface) && glyphTypeface != null) + if (TryGetWithWeight(weight + i, out glyphTypeface)) { return true; } @@ -775,7 +818,7 @@ namespace Avalonia.Media.Fonts //If no match is found, look for available weights less than the target, in descending order. for (var i = 0; weight - i >= 100; i += 50) { - if (glyphTypefaces.TryGetValue(key with { Weight = (FontWeight)(weight - i) }, out glyphTypeface) && glyphTypeface != null) + if (TryGetWithWeight(weight - i, out glyphTypeface)) { return true; } @@ -784,7 +827,7 @@ namespace Avalonia.Media.Fonts //If no match is found, look for available weights greater than 500, in ascending order. for (var i = 0; weight + i <= 900; i += 50) { - if (glyphTypefaces.TryGetValue(key with { Weight = (FontWeight)(weight + i) }, out glyphTypeface) && glyphTypeface != null) + if (TryGetWithWeight(weight + i, out glyphTypeface)) { return true; } @@ -796,7 +839,7 @@ namespace Avalonia.Media.Fonts { for (var i = 0; weight - i >= 100; i += 50) { - if (glyphTypefaces.TryGetValue(key with { Weight = (FontWeight)(weight - i) }, out glyphTypeface) && glyphTypeface != null) + if (TryGetWithWeight(weight - i, out glyphTypeface)) { return true; } @@ -805,7 +848,7 @@ namespace Avalonia.Media.Fonts //If no match is found, look for available weights less than the target, in descending order. for (var i = 0; weight + i <= 900; i += 50) { - if (glyphTypefaces.TryGetValue(key with { Weight = (FontWeight)(weight + i) }, out glyphTypeface) && glyphTypeface != null) + if (TryGetWithWeight(weight + i, out glyphTypeface)) { return true; } @@ -817,7 +860,7 @@ namespace Avalonia.Media.Fonts { for (var i = 0; weight + i <= 900; i += 50) { - if (glyphTypefaces.TryGetValue(key with { Weight = (FontWeight)(weight + i) }, out glyphTypeface) && glyphTypeface != null) + if (TryGetWithWeight(weight + i, out glyphTypeface)) { return true; } @@ -826,7 +869,7 @@ namespace Avalonia.Media.Fonts //If no match is found, look for available weights less than the target, in descending order. for (var i = 0; weight - i >= 100; i += 50) { - if (glyphTypefaces.TryGetValue(key with { Weight = (FontWeight)(weight - i) }, out glyphTypeface) && glyphTypeface != null) + if (TryGetWithWeight(weight - i, out glyphTypeface)) { return true; } @@ -834,6 +877,11 @@ namespace Avalonia.Media.Fonts } return false; + + bool TryGetWithWeight(int effectiveWeight, [NotNullWhen(true)] out GlyphTypeface? glyphTypeface) + => glyphTypefaces.TryGetValue(key with { Weight = (FontWeight)effectiveWeight }, out glyphTypeface) && + glyphTypeface != null && + glyphTypeface.IsLastResort == isLastResort; } void IDisposable.Dispose() diff --git a/src/Avalonia.Base/Media/Fonts/SystemFontCollection.cs b/src/Avalonia.Base/Media/Fonts/SystemFontCollection.cs index cf055e5d99..1c79127ec3 100644 --- a/src/Avalonia.Base/Media/Fonts/SystemFontCollection.cs +++ b/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,7 +52,18 @@ namespace Avalonia.Media.Fonts return false; } - glyphTypeface = new GlyphTypeface(platformTypeface); + // 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) + { + return false; + } //Add to cache with platform typeface family name first TryAddGlyphTypeface(platformTypeface.FamilyName, key, glyphTypeface); @@ -73,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? familyTypefaces) @@ -112,7 +123,10 @@ namespace Avalonia.Media.Fonts } // Not in cache yet: create glyph typeface and try to add it. - var glyphTypeface = new GlyphTypeface(platformTypeface); + if (GlyphTypeface.TryCreate(platformTypeface) is not { } glyphTypeface) + { + return false; + } // Try adding with the platform typeface family name first. TryAddGlyphTypeface(platformTypeface.FamilyName, key, glyphTypeface); diff --git a/src/Avalonia.Base/Media/Fonts/Tables/Cmap/CharacterToGlyphMap.cs b/src/Avalonia.Base/Media/Fonts/Tables/Cmap/CharacterToGlyphMap.cs index 0f04e90e1b..973e99351a 100644 --- a/src/Avalonia.Base/Media/Fonts/Tables/Cmap/CharacterToGlyphMap.cs +++ b/src/Avalonia.Base/Media/Fonts/Tables/Cmap/CharacterToGlyphMap.cs @@ -1,7 +1,5 @@ using System; -using System.Collections.Generic; using System.Runtime.CompilerServices; -using System.Text; namespace Avalonia.Media.Fonts.Tables.Cmap { @@ -16,9 +14,10 @@ namespace Avalonia.Media.Fonts.Tables.Cmap public readonly struct CharacterToGlyphMap #pragma warning restore CA1815 // Override equals not needed for readonly struct { - private readonly CmapFormat _format; private readonly CmapFormat4Table? _format4; - private readonly CmapFormat12Table? _format12; + private readonly CmapFormat12Or13Table? _format12Or13; + + internal CmapFormat Format { get; } /// /// Initializes a new instance of the CharacterToGlyphMap class using the specified Format 4 cmap table. @@ -27,21 +26,21 @@ namespace Avalonia.Media.Fonts.Tables.Cmap [MethodImpl(MethodImplOptions.AggressiveInlining)] internal CharacterToGlyphMap(CmapFormat4Table table) { - _format = CmapFormat.Format4; + Format = CmapFormat.Format4; _format4 = table; - _format12 = null; + _format12Or13 = null; } /// /// Initializes a new instance of the CharacterToGlyphMap class using the specified Format 12 character-to-glyph /// mapping table. /// - /// The Format 12 cmap table that defines the mapping from Unicode code points to glyph indices. Cannot be null. + /// The Format 12 or 13 cmap table that defines the mapping from Unicode code points to glyph indices. Cannot be null. [MethodImpl(MethodImplOptions.AggressiveInlining)] - internal CharacterToGlyphMap(CmapFormat12Table table) + internal CharacterToGlyphMap(CmapFormat12Or13Table table) { - _format = CmapFormat.Format12; - _format12 = table; + Format = table.Format; + _format12Or13 = table; _format4 = null; } @@ -65,10 +64,10 @@ namespace Avalonia.Media.Fonts.Tables.Cmap [MethodImpl(MethodImplOptions.AggressiveInlining)] public ushort GetGlyph(int codePoint) { - return _format switch + return Format switch { CmapFormat.Format4 => _format4!.GetGlyph(codePoint), - CmapFormat.Format12 => _format12!.GetGlyph(codePoint), + CmapFormat.Format12 or CmapFormat.Format13 => _format12Or13!.GetGlyph(codePoint), _ => 0 }; } @@ -81,10 +80,10 @@ namespace Avalonia.Media.Fonts.Tables.Cmap [MethodImpl(MethodImplOptions.AggressiveInlining)] public bool ContainsGlyph(int codePoint) { - return _format switch + return Format switch { CmapFormat.Format4 => _format4!.ContainsGlyph(codePoint), - CmapFormat.Format12 => _format12!.ContainsGlyph(codePoint), + CmapFormat.Format12 or CmapFormat.Format13 => _format12Or13!.ContainsGlyph(codePoint), _ => false }; } @@ -102,20 +101,20 @@ namespace Avalonia.Media.Fonts.Tables.Cmap [MethodImpl(MethodImplOptions.AggressiveInlining)] public void GetGlyphs(ReadOnlySpan codePoints, Span glyphIds) { - switch (_format) + switch (Format) { case CmapFormat.Format4: _format4!.GetGlyphs(codePoints, glyphIds); return; case CmapFormat.Format12: - _format12!.GetGlyphs(codePoints, glyphIds); + case CmapFormat.Format13: + _format12Or13!.GetGlyphs(codePoints, glyphIds); return; default: glyphIds.Clear(); return; } } - /// /// Attempts to retrieve the glyph identifier corresponding to the specified Unicode code point. @@ -127,10 +126,10 @@ namespace Avalonia.Media.Fonts.Tables.Cmap [MethodImpl(MethodImplOptions.AggressiveInlining)] public bool TryGetGlyph(int codePoint, out ushort glyphId) { - switch (_format) + switch (Format) { case CmapFormat.Format4: return _format4!.TryGetGlyph(codePoint, out glyphId); - case CmapFormat.Format12: return _format12!.TryGetGlyph(codePoint, out glyphId); + case CmapFormat.Format12 or CmapFormat.Format13: return _format12Or13!.TryGetGlyph(codePoint, out glyphId); default: glyphId = 0; return false; } } @@ -142,7 +141,7 @@ namespace Avalonia.Media.Fonts.Tables.Cmap [MethodImpl(MethodImplOptions.AggressiveInlining)] public CodepointRangeEnumerator GetMappedRanges() { - return new CodepointRangeEnumerator(_format, _format4, _format12); + return new CodepointRangeEnumerator(Format, _format4, _format12Or13); } /// diff --git a/src/Avalonia.Base/Media/Fonts/Tables/Cmap/CmapFormat12Table.cs b/src/Avalonia.Base/Media/Fonts/Tables/Cmap/CmapFormat12Or13Table.cs similarity index 88% rename from src/Avalonia.Base/Media/Fonts/Tables/Cmap/CmapFormat12Table.cs rename to src/Avalonia.Base/Media/Fonts/Tables/Cmap/CmapFormat12Or13Table.cs index b4440e7884..cc20e735d5 100644 --- a/src/Avalonia.Base/Media/Fonts/Tables/Cmap/CmapFormat12Table.cs +++ b/src/Avalonia.Base/Media/Fonts/Tables/Cmap/CmapFormat12Or13Table.cs @@ -1,30 +1,31 @@ using System; using System.Buffers.Binary; -using System.Collections; -using System.Collections.Generic; using System.Diagnostics; using System.Runtime.CompilerServices; namespace Avalonia.Media.Fonts.Tables.Cmap { - internal sealed class CmapFormat12Table + internal sealed class CmapFormat12Or13Table { private readonly ReadOnlyMemory _table; private readonly int _groupCount; private readonly ReadOnlyMemory _groups; + public CmapFormat Format { get; } + /// /// Gets the language code for the cmap subtable. /// For non-language-specific tables, this value is 0. /// public uint Language { get; } - public CmapFormat12Table(ReadOnlyMemory table) + public CmapFormat12Or13Table(ReadOnlyMemory table) { var reader = new BigEndianBinaryReader(table.Span); ushort format = reader.ReadUInt16(); - Debug.Assert(format == 12, "Format must be 12."); + Debug.Assert(format is 12 or 13, "Format must be 12 or 13."); + Format = (CmapFormat)format; ushort reserved = reader.ReadUInt16(); Debug.Assert(reserved == 0, "Reserved field must be 0."); @@ -101,7 +102,7 @@ namespace Avalonia.Media.Fonts.Tables.Cmap // Optimization: check if codepoint is in the same group as previous if (lastGroup >= 0 && codePoint >= lastStart && codePoint <= lastEnd) { - glyphIds[i] = (ushort)(lastStartGlyph + (codePoint - lastStart)); + glyphIds[i] = CalcEffectiveGlyph(codePoint, lastStart, lastStartGlyph); continue; } @@ -122,27 +123,13 @@ namespace Avalonia.Media.Fonts.Tables.Cmap lastEnd = ReadUInt32BE(groups, groupIndex, 4); lastStartGlyph = ReadUInt32BE(groups, groupIndex, 8); - glyphIds[i] = (ushort)(lastStartGlyph + (codePoint - lastStart)); + glyphIds[i] = CalcEffectiveGlyph(codePoint, lastStart, lastStartGlyph); } } public bool TryGetGlyph(int codePoint, out ushort glyphId) { - int groupIndex = FindGroupIndex(codePoint); - - if (groupIndex < 0) - { - glyphId = 0; - return false; - } - - var groups = _groups.Span; - - uint start = ReadUInt32BE(groups, groupIndex, 0); - uint startGlyph = ReadUInt32BE(groups, groupIndex, 8); - - glyphId = (ushort)(startGlyph + (codePoint - start)); - + glyphId = this[codePoint]; return glyphId != 0; } @@ -180,10 +167,21 @@ namespace Avalonia.Media.Fonts.Tables.Cmap uint start = ReadUInt32BE(groups, groupIndex, 0); uint startGlyph = ReadUInt32BE(groups, groupIndex, 8); + return CalcEffectiveGlyph(codePoint, start, startGlyph); + } + } - // Calculate glyph index - return (ushort)(startGlyph + (codePoint - start)); + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private ushort CalcEffectiveGlyph(int codePoint, uint start, uint startGlyph) + { + // Format 13, all codepoints in the group map to a single glyph + if (Format == CmapFormat.Format13) + { + return (ushort)startGlyph; } + + // Format 12, calculate glyph index + return (ushort)(startGlyph + (codePoint - start)); } // Optimized binary search that works directly with cached span diff --git a/src/Avalonia.Base/Media/Fonts/Tables/Cmap/CmapTable.cs b/src/Avalonia.Base/Media/Fonts/Tables/Cmap/CmapTable.cs index f526658133..7774294e76 100644 --- a/src/Avalonia.Base/Media/Fonts/Tables/Cmap/CmapTable.cs +++ b/src/Avalonia.Base/Media/Fonts/Tables/Cmap/CmapTable.cs @@ -49,22 +49,28 @@ namespace Avalonia.Media.Fonts.Tables.Cmap } // Try to find the best Format 12 subtable entry - if (TryFindFormat12Entry(entries, out var format12Entry)) + if (TryFindFormat12Or13Entry(entries, CmapFormat.Format12, out var format12Entry)) { // Prefer Format 12 if available - return new CharacterToGlyphMap(new CmapFormat12Table(format12Entry.GetSubtableMemory(table))); + return new CharacterToGlyphMap(new CmapFormat12Or13Table(format12Entry.GetSubtableMemory(table))); } - // Fallback to Format 4 + // Then Format 4 if (TryFindFormat4Entry(entries, out var format4Entry)) { return new CharacterToGlyphMap(new CmapFormat4Table(format4Entry.GetSubtableMemory(table))); } + // Fallback to Format 13, which is a "last resort" format mapping many codepoints to a single glyph + if (TryFindFormat12Or13Entry(entries, CmapFormat.Format13, out var format13Entry)) + { + return new CharacterToGlyphMap(new CmapFormat12Or13Table(format13Entry.GetSubtableMemory(table))); + } + throw new InvalidOperationException("No suitable cmap subtable found."); // Tries to find the best Format 12 subtable entry based on platform and encoding preferences - static bool TryFindFormat12Entry(CmapSubtableEntry[] entries, out CmapSubtableEntry result) + static bool TryFindFormat12Or13Entry(CmapSubtableEntry[] entries, CmapFormat expectedFormat, out CmapSubtableEntry result) { result = default; var foundPlatformScore = int.MaxValue; @@ -72,7 +78,7 @@ namespace Avalonia.Media.Fonts.Tables.Cmap foreach (var entry in entries) { - if (entry.Format != CmapFormat.Format12) + if (entry.Format != expectedFormat) { continue; } diff --git a/src/Avalonia.Base/Media/Fonts/Tables/Cmap/CodepointRangeEnumerator.cs b/src/Avalonia.Base/Media/Fonts/Tables/Cmap/CodepointRangeEnumerator.cs index b631c264d1..627ce1b6b6 100644 --- a/src/Avalonia.Base/Media/Fonts/Tables/Cmap/CodepointRangeEnumerator.cs +++ b/src/Avalonia.Base/Media/Fonts/Tables/Cmap/CodepointRangeEnumerator.cs @@ -6,21 +6,21 @@ namespace Avalonia.Media.Fonts.Tables.Cmap /// Enumerates contiguous ranges of Unicode code points present in a character map (cmap) table. /// /// This enumerator is typically used to iterate over all code point ranges defined by a cmap - /// table in an OpenType or TrueType font. It supports both Format 4 and Format 12 cmap subtables. The enumerator is - /// a ref struct and must be used within the stack context; it cannot be stored on the heap or used across await or - /// yield boundaries. + /// table in an OpenType or TrueType font. It supports Format 4, Format 12, and Format 13 cmap subtables. + /// The enumerator is a ref struct and must be used within the stack context; it cannot be stored on the + /// heap or used across await or yield boundaries. public ref struct CodepointRangeEnumerator { private readonly CmapFormat _format; private readonly CmapFormat4Table? _f4; - private readonly CmapFormat12Table? _f12; + private readonly CmapFormat12Or13Table? _f12Or13; private int _index; - internal CodepointRangeEnumerator(CmapFormat format, CmapFormat4Table? f4, CmapFormat12Table? f12) + internal CodepointRangeEnumerator(CmapFormat format, CmapFormat4Table? f4, CmapFormat12Or13Table? f12Or13) { _format = format; _f4 = f4; - _f12 = f12; + _f12Or13 = f12Or13; _index = -1; } @@ -52,8 +52,9 @@ namespace Avalonia.Media.Fonts.Tables.Cmap return result; } case CmapFormat.Format12: + case CmapFormat.Format13: { - var result = _f12!.TryGetRange(_index, out var range); + var result = _f12Or13!.TryGetRange(_index, out var range); Current = range; diff --git a/src/Avalonia.Base/Media/GlyphRun.cs b/src/Avalonia.Base/Media/GlyphRun.cs index 41eed5b747..cccef8f938 100644 --- a/src/Avalonia.Base/Media/GlyphRun.cs +++ b/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 concreteList) { return CollectionsMarshal.AsSpan(concreteList); } -#endif array = new ushort[count]; for (var i = 0; i < count; ++i) diff --git a/src/Avalonia.Base/Media/GlyphTypeface.cs b/src/Avalonia.Base/Media/GlyphTypeface.cs index 4922c41cbd..01fb3a8e1f 100644 --- a/src/Avalonia.Base/Media/GlyphTypeface.cs +++ b/src/Avalonia.Base/Media/GlyphTypeface.cs @@ -2,7 +2,7 @@ using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; using System.Globalization; -using System.Runtime.CompilerServices; +using Avalonia.Logging; using Avalonia.Media.Fonts; using Avalonia.Media.Fonts.Tables; using Avalonia.Media.Fonts.Tables.Cmap; @@ -132,15 +132,21 @@ namespace Avalonia.Media } } + HeadTable.TryLoad(this, out var headTable); + + IsLastResort = (headTable is not null && (headTable.Flags & HeadFlags.LastResortFont) != 0) || + _cmapTable.Format == CmapFormat.Format13; + var postTable = PostTable.Load(this); var isFixedPitch = postTable.IsFixedPitch; var underlineOffset = postTable.UnderlinePosition; var underlineSize = postTable.UnderlineThickness; + var designEmHeight = GetFontDesignEmHeight(headTable); Metrics = new FontMetrics { - DesignEmHeight = headTable?.UnitsPerEm ?? 0, + DesignEmHeight = designEmHeight, Ascent = ascent, Descent = descent, LineGap = lineGap, @@ -240,6 +246,37 @@ namespace Avalonia.Media } } + private static ushort GetFontDesignEmHeight(HeadTable? headTable) + { + var unitsPerEm = headTable?.UnitsPerEm ?? 0; + + // Bitmap fonts may specify 0 or miss the head table completely. + // Use 2048 as sensible default (used by most fonts). + if (unitsPerEm == 0) + unitsPerEm = 2048; + + return unitsPerEm; + } + + internal static GlyphTypeface? TryCreate(IPlatformTypeface typeface, FontSimulations fontSimulations = FontSimulations.None) + { + try + { + return new GlyphTypeface(typeface, fontSimulations); + } + catch (Exception ex) + { + Logger.TryGet(LogEventLevel.Warning, LogArea.Fonts)?.Log( + null, + "Could not create glyph typeface from platform typeface named {FamilyName} with simulations {Simulations}: {Exception}", + typeface.FamilyName, + fontSimulations, + ex); + + return null; + } + } + /// /// Gets the family name of the font. /// @@ -353,6 +390,11 @@ namespace Avalonia.Media } } + /// + /// Gets whether the font should be used as a last resort, if no other fonts matched. + /// + internal bool IsLastResort { get; } + /// /// Attempts to retrieve the horizontal advance width for the specified glyph. /// diff --git a/src/Avalonia.Base/Media/Imaging/Bitmap.cs b/src/Avalonia.Base/Media/Imaging/Bitmap.cs index 63ec02737f..dc1541414b 100644 --- a/src/Avalonia.Base/Media/Imaging/Bitmap.cs +++ b/src/Avalonia.Base/Media/Imaging/Bitmap.cs @@ -178,12 +178,8 @@ namespace Avalonia.Media.Imaging public virtual AlphaFormat? AlphaFormat => (PlatformImpl.Item as IReadableBitmapImpl)?.AlphaFormat; - private protected unsafe void CopyPixelsCore(PixelRect sourceRect, IntPtr buffer, int bufferSize, int stride, - ILockedFramebuffer fb) + private PixelRect ValidateSourceRect(PixelRect sourceRect) { - if (Format == null) - throw new NotSupportedException("CopyPixels is not supported for this bitmap type"); - if ((sourceRect.Width <= 0 || sourceRect.Height <= 0) && (sourceRect.X != 0 || sourceRect.Y != 0)) throw new ArgumentOutOfRangeException(nameof(sourceRect)); @@ -197,6 +193,16 @@ namespace Avalonia.Media.Imaging if (sourceRect.Right > PixelSize.Width || sourceRect.Bottom > PixelSize.Height) throw new ArgumentOutOfRangeException(nameof(sourceRect)); + return sourceRect; + } + + private protected unsafe void CopyPixelsCore(PixelRect sourceRect, IntPtr buffer, int bufferSize, int stride, + ILockedFramebuffer fb) + { + if (Format == null) + throw new NotSupportedException("CopyPixels is not supported for this bitmap type"); + + sourceRect = ValidateSourceRect(sourceRect); int minStride = checked(((sourceRect.Width * fb.Format.BitsPerPixel) + 7) / 8); if (stride < minStride) @@ -223,8 +229,10 @@ namespace Avalonia.Media.Imaging || PlatformImpl.Item is not IReadableBitmapImpl readable || Format != readable.Format ) + { throw new NotSupportedException("CopyPixels is not supported for this bitmap type"); - + } + if (_isTranscoded) throw new NotSupportedException("CopyPixels is not supported for transcoded bitmaps"); @@ -241,7 +249,13 @@ namespace Avalonia.Media.Imaging { if (PlatformImpl.Item is not IReadableBitmapImpl readable || readable.Format == null || readable.AlphaFormat == null) { - throw new NotSupportedException("CopyPixels is not supported for this bitmap type"); + // Since we can't read pixels from the bitmap, we need to render it to a compatible bitmap and read pixels from it. + using var rtb = new RenderTargetBitmap(PixelSize); + using (var ctx = rtb.CreateDrawingContext()) + ctx.DrawImage(this, new Rect(rtb.Size)); + rtb.CopyPixels(buffer); + + return; } if (buffer.Format != readable.Format || buffer.AlphaFormat != readable.AlphaFormat) diff --git a/src/Avalonia.Base/Media/Imaging/RenderTargetBitmap.cs b/src/Avalonia.Base/Media/Imaging/RenderTargetBitmap.cs index 26229b5ecb..98f90c7768 100644 --- a/src/Avalonia.Base/Media/Imaging/RenderTargetBitmap.cs +++ b/src/Avalonia.Base/Media/Imaging/RenderTargetBitmap.cs @@ -77,7 +77,7 @@ namespace Avalonia.Media.Imaging /// The drawing context. public DrawingContext CreateDrawingContext(bool clear) { - var platform = PlatformImpl.Item.CreateDrawingContext(true); + var platform = PlatformImpl.Item.CreateDrawingContext(); if(clear) platform.Clear(Colors.Transparent); return new PlatformDrawingContext(platform); diff --git a/src/Avalonia.Base/Media/TextFormatting/FormattedTextSource.cs b/src/Avalonia.Base/Media/TextFormatting/FormattedTextSource.cs index 1586639fbd..13fc22b3e5 100644 --- a/src/Avalonia.Base/Media/TextFormatting/FormattedTextSource.cs +++ b/src/Avalonia.Base/Media/TextFormatting/FormattedTextSource.cs @@ -132,5 +132,71 @@ namespace Avalonia.Media.TextFormatting return Math.Min(length, text.Length); } + + /// + /// References a portion of a text buffer. + /// + private readonly record struct TextRange + { + public TextRange(int start, int length) + { + Start = start; + Length = length; + } + + /// + /// Gets the start. + /// + /// + /// The start. + /// + public int Start { get; } + + /// + /// Gets the length. + /// + /// + /// The length. + /// + public int Length { get; } + + /// + /// Gets the end. + /// + /// + /// The end. + /// + public int End => Start + Length - 1; + + /// + /// Returns a specified number of contiguous elements from the start of the slice. + /// + /// The number of elements to return. + /// A that contains the specified number of elements from the start of this slice. + public TextRange Take(int length) + { + if (length > Length) + { + throw new ArgumentOutOfRangeException(nameof(length)); + } + + return new TextRange(Start, length); + } + + /// + /// Bypasses a specified number of elements in the slice and then returns the remaining elements. + /// + /// The number of elements to skip before returning the remaining elements. + /// A that contains the elements that occur after the specified index in this slice. + public TextRange Skip(int length) + { + if (length > Length) + { + throw new ArgumentOutOfRangeException(nameof(length)); + } + + return new TextRange(Start + length, Length - length); + } + } } } diff --git a/src/Avalonia.Base/Media/TextFormatting/FormattingBufferHelper.cs b/src/Avalonia.Base/Media/TextFormatting/FormattingBufferHelper.cs index c27903cd55..d8672ffba8 100644 --- a/src/Avalonia.Base/Media/TextFormatting/FormattingBufferHelper.cs +++ b/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>(approximateCapacity)) { -#if NET6_0_OR_GREATER dictionary.TrimExcess(); -#else - dictionary = new Dictionary(); -#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 } } } diff --git a/src/Avalonia.Base/Media/TextFormatting/TextLayout.cs b/src/Avalonia.Base/Media/TextFormatting/TextLayout.cs index 24e9019084..802d6ebb9d 100644 --- a/src/Avalonia.Base/Media/TextFormatting/TextLayout.cs +++ b/src/Avalonia.Base/Media/TextFormatting/TextLayout.cs @@ -627,7 +627,6 @@ namespace Avalonia.Media.TextFormatting finally { objectPool.TextLines.Return(ref textLines); - objectPool.VerifyAllReturned(); } } diff --git a/src/Avalonia.Base/Media/TextFormatting/TextRange.cs b/src/Avalonia.Base/Media/TextFormatting/TextRange.cs deleted file mode 100644 index e8bab55aff..0000000000 --- a/src/Avalonia.Base/Media/TextFormatting/TextRange.cs +++ /dev/null @@ -1,70 +0,0 @@ -using System; - -namespace Avalonia.Media.TextFormatting -{ - /// - /// References a portion of a text buffer. - /// - public readonly record struct TextRange - { - public TextRange(int start, int length) - { - Start = start; - Length = length; - } - - /// - /// Gets the start. - /// - /// - /// The start. - /// - public int Start { get; } - - /// - /// Gets the length. - /// - /// - /// The length. - /// - public int Length { get; } - - /// - /// Gets the end. - /// - /// - /// The end. - /// - public int End => Start + Length - 1; - - /// - /// Returns a specified number of contiguous elements from the start of the slice. - /// - /// The number of elements to return. - /// A that contains the specified number of elements from the start of this slice. - public TextRange Take(int length) - { - if (length > Length) - { - throw new ArgumentOutOfRangeException(nameof(length)); - } - - return new TextRange(Start, length); - } - - /// - /// Bypasses a specified number of elements in the slice and then returns the remaining elements. - /// - /// The number of elements to skip before returning the remaining elements. - /// A that contains the elements that occur after the specified index in this slice. - public TextRange Skip(int length) - { - if (length > Length) - { - throw new ArgumentOutOfRangeException(nameof(length)); - } - - return new TextRange(Start + length, Length - length); - } - } -} diff --git a/src/Avalonia.Base/Media/Typeface.cs b/src/Avalonia.Base/Media/Typeface.cs index 1adcac5b75..f745ba2d23 100644 --- a/src/Avalonia.Base/Media/Typeface.cs +++ b/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(token, true, out var newStyle)) + if (Enum.TryParse(token, true, out var newStyle)) { style = newStyle; match = true; } - else if (EnumHelper.TryParse(token, true, out var newWeight)) + else if (Enum.TryParse(token, true, out var newWeight)) { weight = newWeight; match = true; } - else if (EnumHelper.TryParse(token, true, out var newStretch)) + else if (Enum.TryParse(token, true, out var newStretch)) { stretch = newStretch; match = true; diff --git a/src/Avalonia.Base/Metadata/PrivateApiAttribute.cs b/src/Avalonia.Base/Metadata/PrivateApiAttribute.cs index 8246d4a18d..9685ed3817 100644 --- a/src/Avalonia.Base/Metadata/PrivateApiAttribute.cs +++ b/src/Avalonia.Base/Metadata/PrivateApiAttribute.cs @@ -2,7 +2,7 @@ using System; namespace Avalonia.Metadata; -[AttributeUsage(AttributeTargets.Interface | AttributeTargets.Class | AttributeTargets.Constructor +[AttributeUsage(AttributeTargets.Interface | AttributeTargets.Class | AttributeTargets.Constructor | AttributeTargets.Enum | AttributeTargets.Method | AttributeTargets.Property | AttributeTargets.Field | AttributeTargets.Struct)] public sealed class PrivateApiAttribute : Attribute { diff --git a/src/Avalonia.Base/Platform/ICursorFactory.cs b/src/Avalonia.Base/Platform/ICursorFactory.cs index 99a9a9d7fa..82f54a7b71 100644 --- a/src/Avalonia.Base/Platform/ICursorFactory.cs +++ b/src/Avalonia.Base/Platform/ICursorFactory.cs @@ -1,4 +1,5 @@ using Avalonia.Input; +using Avalonia.Media.Imaging; using Avalonia.Metadata; #nullable enable @@ -9,6 +10,6 @@ namespace Avalonia.Platform public interface ICursorFactory { ICursorImpl GetCursor(StandardCursorType cursorType); - ICursorImpl CreateCursor(IBitmapImpl cursor, PixelPoint hotSpot); + ICursorImpl CreateCursor(Bitmap cursor, PixelPoint hotSpot); } } diff --git a/src/Avalonia.Base/Platform/IDrawingContextImpl.cs b/src/Avalonia.Base/Platform/IDrawingContextImpl.cs index 848620dae2..54310eefa6 100644 --- a/src/Avalonia.Base/Platform/IDrawingContextImpl.cs +++ b/src/Avalonia.Base/Platform/IDrawingContextImpl.cs @@ -228,7 +228,7 @@ namespace Avalonia.Platform (T?)context.GetFeature(typeof(T)); } - public interface IDrawingContextLayerImpl : IRenderTargetBitmapImpl + public interface IDrawingContextLayerImpl : IBitmapImpl { /// /// Does optimized blit with Src blend mode. @@ -240,6 +240,16 @@ namespace Avalonia.Platform /// Returns true if layer supports optimized blit. /// bool CanBlit { get; } + + /// + /// Indicates if the render target is no longer usable and needs to be recreated + /// + bool IsCorrupted { get; } + + /// + /// Creates drawing context. It matches the properties of the original drawing context this layer was created from. + /// + IDrawingContextImpl CreateDrawingContext(); } public interface IDrawingContextLayerWithRenderContextAffinityImpl : IDrawingContextLayerImpl diff --git a/src/Avalonia.Base/Platform/IPlatformRenderInterface.cs b/src/Avalonia.Base/Platform/IPlatformRenderInterface.cs index 3a42a88aed..bffc00235b 100644 --- a/src/Avalonia.Base/Platform/IPlatformRenderInterface.cs +++ b/src/Avalonia.Base/Platform/IPlatformRenderInterface.cs @@ -5,6 +5,7 @@ using Avalonia.Media; using Avalonia.Media.Imaging; using Avalonia.Media.TextFormatting; using Avalonia.Metadata; +using Avalonia.Platform.Surfaces; namespace Avalonia.Platform { @@ -214,7 +215,7 @@ namespace Avalonia.Platform /// The list of native platform surfaces that can be used for output. /// /// An . - IRenderTarget CreateRenderTarget(IEnumerable surfaces); + IRenderTarget CreateRenderTarget(IEnumerable surfaces); /// /// Creates an offscreen render target @@ -239,5 +240,10 @@ namespace Avalonia.Platform /// Maximum supported offscreen render target pixel size, or null if no limit /// public PixelSize? MaxOffscreenRenderTargetPixelSize { get; } + + /// + /// Checks if a render target can be created for the given surfaces and the preferred surface is ready + /// + bool IsReadyToCreateRenderTarget(IEnumerable surfaces) => true; } } diff --git a/src/Avalonia.Base/Platform/IRenderTarget.cs b/src/Avalonia.Base/Platform/IRenderTarget.cs index a31e7e550a..e66d14995e 100644 --- a/src/Avalonia.Base/Platform/IRenderTarget.cs +++ b/src/Avalonia.Base/Platform/IRenderTarget.cs @@ -25,16 +25,18 @@ namespace Avalonia.Platform /// /// Creates an for a rendering session. /// - /// Apply DPI reported by the render target as a hidden transform matrix - IDrawingContextImpl CreateDrawingContext(bool useScaledDrawing); + /// Information about the scene that's about to be rendered into this render target. + /// This is expected to be reported to the underlying platform and affect the framebuffer size, however + /// the implementation may choose to ignore that information. + /// + /// Returns various properties about the returned drawing context + IDrawingContextImpl CreateDrawingContext(RenderTargetSceneInfo sceneInfo, out RenderTargetDrawingContextProperties properties); /// - /// Creates an for a rendering session. + /// Indicates if the render target is currently ready to be rendered to /// - /// The pixel size of the surface - /// Returns various properties about the returned drawing context - IDrawingContextImpl CreateDrawingContext( - PixelSize expectedPixelSize, - out RenderTargetDrawingContextProperties properties); + bool IsReady => true; + + public record struct RenderTargetSceneInfo(PixelSize Size, double Scaling); } } diff --git a/src/Avalonia.Base/Platform/IRenderTargetBitmapImpl.cs b/src/Avalonia.Base/Platform/IRenderTargetBitmapImpl.cs index d33c503650..aab734c7c9 100644 --- a/src/Avalonia.Base/Platform/IRenderTargetBitmapImpl.cs +++ b/src/Avalonia.Base/Platform/IRenderTargetBitmapImpl.cs @@ -7,7 +7,8 @@ namespace Avalonia.Platform /// . /// [Unstable] - public interface IRenderTargetBitmapImpl : IBitmapImpl, IRenderTarget + public interface IRenderTargetBitmapImpl : IReadableBitmapImpl { + IDrawingContextImpl CreateDrawingContext(); } } diff --git a/src/Avalonia.Base/Platform/Internal/UnmanagedBlob.cs b/src/Avalonia.Base/Platform/Internal/UnmanagedBlob.cs index eeba160a3c..a1296c2ee1 100644 --- a/src/Avalonia.Base/Platform/Internal/UnmanagedBlob.cs +++ b/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 } } } diff --git a/src/Avalonia.Controls/Platform/ManagedDispatcherImpl.cs b/src/Avalonia.Base/Platform/ManagedDispatcherImpl.cs similarity index 100% rename from src/Avalonia.Controls/Platform/ManagedDispatcherImpl.cs rename to src/Avalonia.Base/Platform/ManagedDispatcherImpl.cs diff --git a/src/Avalonia.Base/Platform/StandardRuntimePlatform.cs b/src/Avalonia.Base/Platform/StandardRuntimePlatform.cs index b72e10c831..fc44cbbbd7 100644 --- a/src/Avalonia.Base/Platform/StandardRuntimePlatform.cs +++ b/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() }; } } diff --git a/src/Avalonia.Base/Platform/StandardRuntimePlatformServices.cs b/src/Avalonia.Base/Platform/StandardRuntimePlatformServices.cs index 70919bc477..666cdf2bed 100644 --- a/src/Avalonia.Base/Platform/StandardRuntimePlatformServices.cs +++ b/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; diff --git a/src/Avalonia.Base/Platform/Storage/FileIO/BclLauncher.cs b/src/Avalonia.Base/Platform/Storage/FileIO/BclLauncher.cs index 242b8e6d41..da4cb01df5 100644 --- a/src/Avalonia.Base/Platform/Storage/FileIO/BclLauncher.cs +++ b/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,15 +46,19 @@ internal class BclLauncher : ILauncher ShellExecRaw($"xdg-open \\\"{args}\\\"", waitForExit: false); return true; } - else if (OperatingSystemEx.IsWindows() || OperatingSystemEx.IsMacOS()) + else if (OperatingSystem.IsWindows() || OperatingSystem.IsMacOS()) { - using var process = Process.Start(new ProcessStartInfo + var info = new ProcessStartInfo { - FileName = OperatingSystemEx.IsWindows() ? urlOrFile : "open", - Arguments = OperatingSystemEx.IsMacOS() ? $"{urlOrFile}" : "", + 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 (OperatingSystem.IsMacOS()) + info.ArgumentList.Add(urlOrFile); + using var process = Process.Start(info); return true; } else diff --git a/src/Avalonia.Base/Platform/Storage/FileIO/BclStorageProvider.cs b/src/Avalonia.Base/Platform/Storage/FileIO/BclStorageProvider.cs index decb742ed8..a471dba720 100644 --- a/src/Avalonia.Base/Platform/Storage/FileIO/BclStorageProvider.cs +++ b/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"; } diff --git a/src/Avalonia.Base/Platform/Storage/FileIO/SecurityScopedStream.cs b/src/Avalonia.Base/Platform/Storage/FileIO/SecurityScopedStream.cs index 0e0ffa3b1b..ad2bd9f37a 100644 --- a/src/Avalonia.Base/Platform/Storage/FileIO/SecurityScopedStream.cs +++ b/src/Avalonia.Base/Platform/Storage/FileIO/SecurityScopedStream.cs @@ -40,12 +40,10 @@ internal sealed class SecurityScopedStream(FileStream _stream, IDisposable _secu public override Task 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 buffer) => _stream.Read(buffer); public override ValueTask ReadAsync(Memory 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 buffer) => _stream.Write(buffer); public override ValueTask WriteAsync(ReadOnlyMemory 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 } diff --git a/src/Avalonia.Base/Platform/Storage/FileIO/StorageBookmarkHelper.cs b/src/Avalonia.Base/Platform/Storage/FileIO/StorageBookmarkHelper.cs index 78392ec31d..4c43331803 100644 --- a/src/Avalonia.Base/Platform/Storage/FileIO/StorageBookmarkHelper.cs +++ b/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 decodedBookmark; -#if NET6_0_OR_GREATER + // Each base64 character represents 6 bits, but to be safe, var arrayPool = ArrayPool.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.Shared.Return(arrayPool); -#endif } } diff --git a/src/Avalonia.Base/Platform/Surfaces/IFramebufferPlatformSurface.cs b/src/Avalonia.Base/Platform/Surfaces/IFramebufferPlatformSurface.cs new file mode 100644 index 0000000000..794e2cc229 --- /dev/null +++ b/src/Avalonia.Base/Platform/Surfaces/IFramebufferPlatformSurface.cs @@ -0,0 +1,66 @@ +using System; +using Avalonia.Metadata; + +namespace Avalonia.Platform.Surfaces +{ + [Unstable] + public interface IFramebufferPlatformSurface : IPlatformRenderSurface + { + IFramebufferRenderTarget CreateFramebufferRenderTarget(); + } + + + [PrivateApi] + public interface IFramebufferRenderTarget : IDisposable, IPlatformRenderSurfaceRenderTarget + { + /// + /// Provides a framebuffer descriptor for drawing. + /// + /// + /// Contents should be drawn on actual window after disposing + /// + ILockedFramebuffer Lock(IRenderTarget.RenderTargetSceneInfo sceneInfo, out FramebufferLockProperties properties); + + bool RetainsFrameContents => false; + } + + [PrivateApi] + public record struct FramebufferLockProperties(bool PreviousFrameIsRetained); + + /// + /// For simple cases when framebuffer is always available + /// + public class FuncFramebufferRenderTarget : IFramebufferRenderTarget + { + public delegate ILockedFramebuffer LockFramebufferDelegate(IRenderTarget.RenderTargetSceneInfo sceneInfo, out FramebufferLockProperties properties); + private readonly LockFramebufferDelegate _lockFramebuffer; + + public FuncFramebufferRenderTarget(Func lockFramebuffer) : + this((_, out properties) => + { + properties = default; + return lockFramebuffer(); + }) + { + + } + + + public FuncFramebufferRenderTarget(LockFramebufferDelegate lockFramebuffer, bool retainsFrameContents = false) + { + _lockFramebuffer = lockFramebuffer; + RetainsFrameContents = retainsFrameContents; + } + + public void Dispose() + { + // No-op + } + + public ILockedFramebuffer Lock(IRenderTarget.RenderTargetSceneInfo sceneInfo, + out FramebufferLockProperties properties) => _lockFramebuffer(sceneInfo, out properties); + + public bool RetainsFrameContents { get; } + } + +} diff --git a/src/Avalonia.Base/Platform/Surfaces/IPlatformRenderSurface.cs b/src/Avalonia.Base/Platform/Surfaces/IPlatformRenderSurface.cs new file mode 100644 index 0000000000..ff71a700c4 --- /dev/null +++ b/src/Avalonia.Base/Platform/Surfaces/IPlatformRenderSurface.cs @@ -0,0 +1,15 @@ +using Avalonia.Metadata; + +namespace Avalonia.Platform.Surfaces; + +[PrivateApi] +public interface IPlatformRenderSurface +{ + bool IsReady => true; +} + +[PrivateApi] +public interface IPlatformRenderSurfaceRenderTarget +{ + bool IsReady => true; +} diff --git a/src/Avalonia.Base/Reactive/SerialDisposableValue.cs b/src/Avalonia.Base/Reactive/SerialDisposableValue.cs deleted file mode 100644 index 9eaf6343bf..0000000000 --- a/src/Avalonia.Base/Reactive/SerialDisposableValue.cs +++ /dev/null @@ -1,35 +0,0 @@ -using System; -using System.Threading; - -namespace Avalonia.Reactive; - -/// -/// Represents a disposable resource whose underlying disposable resource can be replaced by another disposable resource, causing automatic disposal of the previous underlying disposable resource. -/// -internal sealed class SerialDisposableValue : IDisposable -{ - private IDisposable? _current; - private bool _disposed; - - public IDisposable? Disposable - { - get => _current; - set - { - _current?.Dispose(); - _current = value; - - if (_disposed) - { - _current?.Dispose(); - _current = null; - } - } - } - - public void Dispose() - { - _disposed = true; - _current?.Dispose(); - } -} diff --git a/src/Avalonia.Base/Rect.cs b/src/Avalonia.Base/Rect.cs index 58a8c56c8b..9c901254a6 100644 --- a/src/Avalonia.Base/Rect.cs +++ b/src/Avalonia.Base/Rect.cs @@ -1,7 +1,6 @@ using System; using System.Globalization; using System.Numerics; -using Avalonia.Animation.Animators; using Avalonia.Utilities; namespace Avalonia diff --git a/src/Avalonia.Base/Rendering/Composition/CompositingRenderer.cs b/src/Avalonia.Base/Rendering/Composition/CompositingRenderer.cs index dd81219168..1bff835f17 100644 --- a/src/Avalonia.Base/Rendering/Composition/CompositingRenderer.cs +++ b/src/Avalonia.Base/Rendering/Composition/CompositingRenderer.cs @@ -8,6 +8,8 @@ using System.Threading.Tasks; using Avalonia.Collections; using Avalonia.Collections.Pooled; using Avalonia.Diagnostics; +using Avalonia.Platform; +using Avalonia.Platform.Surfaces; using Avalonia.Media; using Avalonia.Rendering.Composition.Drawing; using Avalonia.Threading; @@ -20,7 +22,7 @@ namespace Avalonia.Rendering.Composition; /// internal class CompositingRenderer : IRendererWithCompositor, IHitTester { - private readonly IRenderRoot _root; + private readonly IPresentationSource _root; private readonly Compositor _compositor; private readonly RenderDataDrawingContext _recorder; private readonly HashSet _dirty = new(); @@ -48,13 +50,12 @@ internal class CompositingRenderer : IRendererWithCompositor, IHitTester /// /// A function returning the list of native platform's surfaces that can be consumed by rendering subsystems. /// - public CompositingRenderer(IRenderRoot root, Compositor compositor, Func> surfaces) + public CompositingRenderer(IPresentationSource root, Compositor compositor, Func> surfaces) { _root = root; _compositor = compositor; _recorder = new(compositor); CompositionTarget = compositor.CreateCompositionTarget(surfaces); - CompositionTarget.Root = ((Visual)root).AttachToCompositor(compositor); _update = Update; Diagnostics = new RendererDiagnostics(); Diagnostics.PropertyChanged += OnDiagnosticsPropertyChanged; @@ -188,13 +189,13 @@ internal class CompositingRenderer : IRendererWithCompositor, IHitTester commit.Rendered.ContinueWith(_ => Dispatcher.UIThread.Post(() => { _queuedSceneInvalidation = false; - SceneInvalidated?.Invoke(this, new SceneInvalidatedEventArgs(_root, new Rect(_root.ClientSize))); + SceneInvalidated?.Invoke(this, new SceneInvalidatedEventArgs(new Rect(_root.ClientSize))); }, DispatcherPriority.Input), CancellationToken.None, TaskContinuationOptions.ExecuteSynchronously, TaskScheduler.Default); } } public void TriggerSceneInvalidatedForUnitTests(Rect rect) => - SceneInvalidated?.Invoke(this, new SceneInvalidatedEventArgs(_root, rect)); + SceneInvalidated?.Invoke(this, new SceneInvalidatedEventArgs(rect)); private void Update() { diff --git a/src/Avalonia.Base/Rendering/Composition/Compositor.Factories.cs b/src/Avalonia.Base/Rendering/Composition/Compositor.Factories.cs index 8a75681c68..b1ff53e5c8 100644 --- a/src/Avalonia.Base/Rendering/Composition/Compositor.Factories.cs +++ b/src/Avalonia.Base/Rendering/Composition/Compositor.Factories.cs @@ -1,5 +1,7 @@ using System; using System.Collections.Generic; +using Avalonia.Platform; +using Avalonia.Platform.Surfaces; using Avalonia.Rendering.Composition.Animations; using Avalonia.Rendering.Composition.Server; @@ -12,9 +14,9 @@ public partial class Compositor /// /// A factory method to create IRenderTarget to be called from the render thread /// - internal CompositionTarget CreateCompositionTarget(Func> surfaces) + internal CompositionTarget CreateCompositionTarget(Func> surfaces) { - return new CompositionTarget(this, new ServerCompositionTarget(_server, surfaces, DiagnosticTextRenderer)); + return new CompositionTarget(this, new ServerCompositionTarget(_server, surfaces)); } public CompositionContainerVisual CreateContainerVisual() => new(this, new ServerCompositionContainerVisual(_server)); diff --git a/src/Avalonia.Base/Rendering/Composition/Compositor.cs b/src/Avalonia.Base/Rendering/Composition/Compositor.cs index e8cd29f195..2acda6c57d 100644 --- a/src/Avalonia.Base/Rendering/Composition/Compositor.cs +++ b/src/Avalonia.Base/Rendering/Composition/Compositor.cs @@ -37,29 +37,12 @@ namespace Avalonia.Rendering.Composition private readonly object _pendingBatchLock = new(); private readonly List _pendingServerCompositorJobs = new(); private readonly List _pendingServerCompositorPostTargetJobs = new(); - private DiagnosticTextRenderer? _diagnosticTextRenderer; private readonly Action _triggerCommitRequested; internal IEasing DefaultEasing { get; } internal Dispatcher Dispatcher { get; } - private DiagnosticTextRenderer? DiagnosticTextRenderer - { - get - { - if (_diagnosticTextRenderer == null) - { - // We are running in some unit test context - if (AvaloniaLocator.Current.GetService() == null) - return null; - _diagnosticTextRenderer = new(Typeface.Default.GlyphTypeface, 12.0); - } - - return _diagnosticTextRenderer; - } - } - internal event Action? AfterCommit; @@ -68,7 +51,7 @@ namespace Avalonia.Rendering.Composition /// [PrivateApi] public Compositor(IPlatformGraphics? gpu, bool useUiThreadForSynchronousCommits = false) - : this(RenderLoop.LocatorAutoInstance, gpu, useUiThreadForSynchronousCommits) + : this(AvaloniaLocator.Current.GetRequiredService(), gpu, useUiThreadForSynchronousCommits) { } diff --git a/src/Avalonia.Base/Rendering/Composition/ElementCompositionPreview.cs b/src/Avalonia.Base/Rendering/Composition/ElementCompositionPreview.cs index 6d9b9e23a8..7de58a6dba 100644 --- a/src/Avalonia.Base/Rendering/Composition/ElementCompositionPreview.cs +++ b/src/Avalonia.Base/Rendering/Composition/ElementCompositionPreview.cs @@ -25,7 +25,7 @@ public static class ElementComposition throw new InvalidOperationException("Composition visuals belong to different compositor instances"); visual.ChildCompositionVisual = compositionVisual; - visual.GetVisualRoot()?.Renderer.RecalculateChildren(visual); + visual.GetPresentationSource()?.Renderer.RecalculateChildren(visual); } /// diff --git a/src/Avalonia.Base/Rendering/Composition/Server/CompositionTargetOverlays.cs b/src/Avalonia.Base/Rendering/Composition/Server/CompositionTargetOverlays.cs index a8c2908b88..9873fe6ff3 100644 --- a/src/Avalonia.Base/Rendering/Composition/Server/CompositionTargetOverlays.cs +++ b/src/Avalonia.Base/Rendering/Composition/Server/CompositionTargetOverlays.cs @@ -17,20 +17,16 @@ internal class CompositionTargetOverlays private Rect? _oldFpsCounterRect; private long _updateStarted; private readonly ServerCompositionTarget _target; - private readonly DiagnosticTextRenderer? _diagnosticTextRenderer; - public CompositionTargetOverlays( - ServerCompositionTarget target, - DiagnosticTextRenderer? diagnosticTextRenderer) + public CompositionTargetOverlays(ServerCompositionTarget target) { _target = target; - _diagnosticTextRenderer = diagnosticTextRenderer; } private RendererDebugOverlays DebugOverlays { get; set; } private FpsCounter? FpsCounter - => _fpsCounter ??= _diagnosticTextRenderer != null ? new FpsCounter(_diagnosticTextRenderer) : null; + => _fpsCounter ??= DiagnosticTextRenderer is { } diagnosticTextRenderer ? new FpsCounter(diagnosticTextRenderer) : null; private FrameTimeGraph? LayoutTimeGraph => _layoutTimeGraph ??= CreateTimeGraph("Layout"); @@ -44,15 +40,29 @@ internal class CompositionTargetOverlays private FrameTimeGraph? UpdateTimeGraph => _updateTimeGraph ??= CreateTimeGraph("TUpdate"); + private DiagnosticTextRenderer? DiagnosticTextRenderer + { + get + { + if (field is null) + { + // We are running in some unit test context + if (AvaloniaLocator.Current.GetService() == null) + return null; + field = new DiagnosticTextRenderer(Typeface.Default.GlyphTypeface, 12.0); + } + return field; + } + } public bool RequireLayer => DebugOverlays.HasAnyFlag(RendererDebugOverlays.DirtyRects); private FrameTimeGraph? CreateTimeGraph(string title) { - if (_diagnosticTextRenderer == null) + if (DiagnosticTextRenderer is not { } diagnosticTextRenderer) return null; - return new FrameTimeGraph(360, new Size(360.0, 64.0), 1000.0 / 60.0, title, _diagnosticTextRenderer); + return new FrameTimeGraph(360, new Size(360.0, 64.0), 1000.0 / 60.0, title, diagnosticTextRenderer); } @@ -73,6 +83,8 @@ internal class CompositionTargetOverlays if ((DebugOverlays & RendererDebugOverlays.RenderTimeGraph) == 0) { _renderTimeGraph?.Reset(); + _compositorUpdateTimeGraph?.Reset(); + _updateTimeGraph?.Reset(); } } @@ -170,4 +182,4 @@ internal class CompositionTargetOverlays LayoutTimeGraph?.AddFrameValue(lastLayoutPassTiming.Elapsed.TotalMilliseconds); } } -} \ No newline at end of file +} diff --git a/src/Avalonia.Base/Rendering/Composition/Server/FpsCounter.cs b/src/Avalonia.Base/Rendering/Composition/Server/FpsCounter.cs index c13e0d04ae..81f41e3a42 100644 --- a/src/Avalonia.Base/Rendering/Composition/Server/FpsCounter.cs +++ b/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()); diff --git a/src/Avalonia.Base/Rendering/Composition/Server/FrameTimeGraph.cs b/src/Avalonia.Base/Rendering/Composition/Server/FrameTimeGraph.cs index 8e283ba5b1..c5672a1860 100644 --- a/src/Avalonia.Base/Rendering/Composition/Server/FrameTimeGraph.cs +++ b/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 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) diff --git a/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionTarget.cs b/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionTarget.cs index b7a1993510..e8ae84eb03 100644 --- a/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionTarget.cs +++ b/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionTarget.cs @@ -8,6 +8,7 @@ using Avalonia.Media; using Avalonia.Media.Imaging; using Avalonia.Media.Immutable; using Avalonia.Platform; +using Avalonia.Platform.Surfaces; using Avalonia.Rendering.Composition.Transport; using Avalonia.Utilities; @@ -20,7 +21,7 @@ namespace Avalonia.Rendering.Composition.Server internal partial class ServerCompositionTarget : IDisposable { private readonly ServerCompositor _compositor; - private readonly Func> _surfaces; + private readonly Func> _surfaces; private CompositionTargetOverlays _overlays; private static long s_nextId = 1; private IRenderTarget? _renderTarget; @@ -38,14 +39,18 @@ namespace Avalonia.Rendering.Composition.Server public ICompositionTargetDebugEvents? DebugEvents { get; set; } public int RenderedVisuals { get; set; } public int VisitedVisuals { get; set; } + + /// + /// Returns true if the target is enabled and has pending work but its render target was not ready. + /// + internal bool IsWaitingForReadyRenderTarget { get; private set; } - public ServerCompositionTarget(ServerCompositor compositor, Func> surfaces, - DiagnosticTextRenderer? diagnosticTextRenderer) + public ServerCompositionTarget(ServerCompositor compositor, Func> surfaces) : base(compositor) { _compositor = compositor; _surfaces = surfaces; - _overlays = new CompositionTargetOverlays(this, diagnosticTextRenderer); + _overlays = new CompositionTargetOverlays(this); var platformRender = AvaloniaLocator.Current.GetService(); if (platformRender?.SupportsRegions == true && compositor.Options.UseRegionDirtyRectClipping != false) @@ -125,6 +130,8 @@ namespace Avalonia.Rendering.Composition.Server public void Render() { + IsWaitingForReadyRenderTarget = false; + if (_disposed) return; @@ -142,10 +149,20 @@ namespace Avalonia.Rendering.Composition.Server try { - _renderTarget ??= _compositor.CreateRenderTarget(_surfaces()); + if (_renderTarget == null) + { + if (!_compositor.IsReadyToCreateRenderTarget(_surfaces())) + { + IsWaitingForReadyRenderTarget = IsEnabled; + return; + } + + _renderTarget = _compositor.CreateRenderTarget(_surfaces()); + } } catch (RenderTargetNotReadyException) { + IsWaitingForReadyRenderTarget = IsEnabled; return; } catch (RenderTargetCorruptedException) @@ -161,13 +178,18 @@ namespace Avalonia.Rendering.Composition.Server if (!_redrawRequested) 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 || !(_renderTarget.Properties.RetainsPreviousFrameContents && _renderTarget.Properties.IsSuitableForDirectRendering); - using (var renderTargetContext = _renderTarget.CreateDrawingContext( - this.PixelSize, out var properties)) + using (var renderTargetContext = _renderTarget.CreateDrawingContext(new(PixelSize, Scaling), out var properties)) using (var renderTiming = Diagnostic.BeginCompositorRenderPass()) { var fullRedraw = false; @@ -204,7 +226,7 @@ namespace Avalonia.Rendering.Composition.Server DirtyRects.FinalizeFrame(renderBounds); if (_layer != null) { - using (var context = _layer.CreateDrawingContext(false)) + using (var context = _layer.CreateDrawingContext()) RenderRootToContextWithClip(context, Root); renderTargetContext.Clear(Colors.Transparent); diff --git a/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionVisual/ServerCompositionVisual.Adorners.cs b/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionVisual/ServerCompositionVisual.Adorners.cs index fe6effbbd4..6f78ca312a 100644 --- a/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionVisual/ServerCompositionVisual.Adorners.cs +++ b/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!); } } -} \ No newline at end of file +} diff --git a/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionVisual/ServerCompositionVisual.ComputedProperties.cs b/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionVisual/ServerCompositionVisual.ComputedProperties.cs index 4b98b0f80e..e2ce331318 100644 --- a/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionVisual/ServerCompositionVisual.ComputedProperties.cs +++ b/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); } -} \ No newline at end of file +} diff --git a/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionVisual/ServerCompositionVisual.DirtyInputs.cs b/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionVisual/ServerCompositionVisual.DirtyInputs.cs index fa8c6047fc..35debea184 100644 --- a/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionVisual/ServerCompositionVisual.DirtyInputs.cs +++ b/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(); } diff --git a/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionVisual/ServerCompositionVisual.Render.cs b/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionVisual/ServerCompositionVisual.Render.cs index d2dd0346d6..8d8b862bb3 100644 --- a/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionVisual/ServerCompositionVisual.Render.cs +++ b/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionVisual/ServerCompositionVisual.Render.cs @@ -94,7 +94,8 @@ partial class ServerCompositionVisual if (visual.Opacity != 1) { - _opacityStack.Push(effectiveOpacity); + _opacityStack.Push(_opacity); + _opacity = effectiveOpacity; _canvas.PushOpacity(visual.Opacity, visual._transformedSubTreeBounds.Value.ToRect()); } diff --git a/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionVisual/ServerCompositionVisual.Update.cs b/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionVisual/ServerCompositionVisual.Update.cs index b8322225bd..f9b65e01e0 100644 --- a/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionVisual/ServerCompositionVisual.Update.cs +++ b/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; } diff --git a/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionVisual/ServerCompositionVisualCache.cs b/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionVisual/ServerCompositionVisualCache.cs index a9a481b5e3..973ac8a834 100644 --- a/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionVisual/ServerCompositionVisualCache.cs +++ b/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionVisual/ServerCompositionVisualCache.cs @@ -193,7 +193,7 @@ internal class ServerCompositionVisualCache // Render to layer if needed if (!_dirtyRectTracker.IsEmpty) { - using var ctx = _layer.CreateDrawingContext(false); + using var ctx = _layer.CreateDrawingContext(); using (_needsFullReRender ? null : _dirtyRectTracker.BeginDraw(ctx)) { ctx.Clear(Colors.Transparent); diff --git a/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositor.UserApis.cs b/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositor.UserApis.cs index d299bed384..e440ffab26 100644 --- a/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositor.UserApis.cs +++ b/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositor.UserApis.cs @@ -62,7 +62,7 @@ internal partial class ServerCompositor try { target = RenderInterface.Value.CreateOffscreenRenderTarget(pixelSize, new(scaling, scaling), true); - using (var canvas = target.CreateDrawingContext(false)) + using (var canvas = target.CreateDrawingContext()) { canvas.Transform = scaleTransform; visual.Render(canvas, LtrbRect.Infinite, null, renderChildren); diff --git a/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositor.cs b/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositor.cs index e23197ff13..b8cc5afca2 100644 --- a/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositor.cs +++ b/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositor.cs @@ -4,6 +4,7 @@ using System.Diagnostics; using System.Threading; using Avalonia.Logging; using Avalonia.Platform; +using Avalonia.Platform.Surfaces; using Avalonia.Rendering.Composition.Animations; using Avalonia.Rendering.Composition.Expressions; using Avalonia.Rendering.Composition.Transport; @@ -43,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, @@ -63,6 +67,7 @@ namespace Avalonia.Rendering.Composition.Server { lock (_batches) _batches.Enqueue(batch); + _renderLoop.Wakeup(); } internal void UpdateServerTime() => ServerNow = Clock.Elapsed; @@ -71,6 +76,7 @@ namespace Avalonia.Rendering.Composition.Server readonly List _reusableToNotifyRenderedList = new(); void ApplyPendingBatches() { + bool hadBatches = false; while (true) { CompositionBatch batch; @@ -118,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 queue, object endMarker) @@ -170,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()) { @@ -181,7 +195,7 @@ namespace Avalonia.Rendering.Composition.Server try { using (Dispatcher.UIThread.DisableProcessing()) - RenderReentrancySafe(catchExceptions); + return RenderReentrancySafe(catchExceptions); } finally { @@ -189,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) { @@ -201,7 +215,7 @@ namespace Avalonia.Rendering.Composition.Server try { _safeThread = Thread.CurrentThread; - RenderCore(catchExceptions); + return RenderCore(catchExceptions); } finally { @@ -234,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); @@ -262,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) @@ -274,12 +299,17 @@ namespace Avalonia.Rendering.Composition.Server _activeTargets.Remove(target); } - public IRenderTarget CreateRenderTarget(IEnumerable surfaces) + public IRenderTarget CreateRenderTarget(IEnumerable surfaces) { using (RenderInterface.EnsureCurrent()) return RenderInterface.CreateRenderTarget(surfaces); } + public bool IsReadyToCreateRenderTarget(IEnumerable surfaces) + { + return RenderInterface.IsReadyToCreateRenderTarget(surfaces); + } + public bool CheckAccess() => _safeThread == Thread.CurrentThread; public void VerifyAccess() { diff --git a/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositorAnimations.cs b/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositorAnimations.cs index 1f2c7dedb8..0e59cd8f03 100644 --- a/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositorAnimations.cs +++ b/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)) diff --git a/src/Avalonia.Base/Rendering/Composition/Transport/BatchStream.cs b/src/Avalonia.Base/Rendering/Composition/Transport/BatchStream.cs index 0231c29bb3..1fcadae1c2 100644 --- a/src/Avalonia.Base/Rendering/Composition/Transport/BatchStream.cs +++ b/src/Avalonia.Base/Rendering/Composition/Transport/BatchStream.cs @@ -33,11 +33,8 @@ static unsafe class UnalignedMemoryHelper { public static T ReadUnaligned(byte* src) where T : unmanaged { -#if NET6_0_OR_GREATER Unsafe.SkipInit(out var rv); -#else - T rv; -#endif + UnalignedMemcpy((byte*)&rv, src, Unsafe.SizeOf()); return rv; } diff --git a/src/Avalonia.Base/Rendering/Composition/Transport/BatchStreamArrayPool.cs b/src/Avalonia.Base/Rendering/Composition/Transport/BatchStreamArrayPool.cs index 6aabb4d168..d9003659a1 100644 --- a/src/Avalonia.Base/Rendering/Composition/Transport/BatchStreamArrayPool.cs +++ b/src/Avalonia.Base/Rendering/Composition/Transport/BatchStreamArrayPool.cs @@ -1,7 +1,9 @@ using System; using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; using System.Linq; using System.Runtime.InteropServices; +using System.Threading; using Avalonia.Platform; using Avalonia.Threading; @@ -12,54 +14,75 @@ namespace Avalonia.Rendering.Composition.Transport; /// internal abstract class BatchStreamPoolBase : IDisposable { + private readonly Action>? _startTimer; readonly Stack _pool = new(); bool _disposed; int _usage; readonly int[] _usageStatistics = new int[10]; int _usageStatisticsSlot; - readonly bool _reclaimImmediately; + private readonly WeakReference> _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>? startTimer = null) { + _startTimer = startTimer; if(!needsFinalize) - GC.SuppressFinalize(needsFinalize); - - var updateRef = new WeakReference>(this); - if ( - reclaimImmediately - || ( - AvaloniaLocator.Current.GetService() == null - && AvaloniaLocator.Current.GetService() == null)) - _reclaimImmediately = true; - else - StartUpdateTimer(startTimer, updateRef); + GC.SuppressFinalize(this); + + _updateRef = new WeakReference>(this); + _reclaimOnDispatcher = !reclaimImmediately ? Dispatcher.FromThread(Thread.CurrentThread) : null; + EnsureUpdateTimer(); } + - static void StartUpdateTimer(Action>? startTimer, WeakReference> updateRef) + void EnsureUpdateTimer() { - Func 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 GetTimerProc(WeakReference> 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); @@ -68,9 +91,17 @@ internal abstract class BatchStreamPoolBase : 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) @@ -91,6 +122,8 @@ internal abstract class BatchStreamPoolBase : IDisposable if (_usageStatistics[_usageStatisticsSlot] < _usage) _usageStatistics[_usageStatisticsSlot] = _usage; + OnActivity(); + if (_pool.Count != 0) return _pool.Pop(); } @@ -104,9 +137,10 @@ internal abstract class BatchStreamPoolBase : IDisposable lock (_pool) { _usage--; - if (!_disposed && !_reclaimImmediately) + if (!_disposed && !ReclaimImmediately) { _pool.Push(item); + OnActivity(); return; } } diff --git a/src/Avalonia.Base/Rendering/DefaultRenderTimer.cs b/src/Avalonia.Base/Rendering/DefaultRenderTimer.cs index 102cc30e87..cc24086305 100644 --- a/src/Avalonia.Base/Rendering/DefaultRenderTimer.cs +++ b/src/Avalonia.Base/Rendering/DefaultRenderTimer.cs @@ -15,8 +15,7 @@ namespace Avalonia.Rendering [PrivateApi] public class DefaultRenderTimer : IRenderTimer { - private int _subscriberCount; - private Action? _tick; + private volatile Action? _tick; private IDisposable? _subscription; /// @@ -36,40 +35,28 @@ namespace Avalonia.Rendering public int FramesPerSecond { get; } /// - public event Action Tick + public Action? 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; } } /// public virtual bool RunsInBackground => true; - /// - /// Starts the timer. - /// - protected void Start() - { - _subscription = StartCore(InternalTick); - } - /// /// Provides the implementation of starting the timer. /// @@ -85,15 +72,6 @@ namespace Avalonia.Rendering return new Timer(_ => tick(TimeSpan.FromMilliseconds(Environment.TickCount)), null, interval, interval); } - /// - /// Stops the timer. - /// - protected void Stop() - { - _subscription?.Dispose(); - _subscription = null; - } - private void InternalTick(TimeSpan tickCount) { _tick?.Invoke(tickCount); diff --git a/src/Avalonia.Base/Rendering/IPresentationSource.cs b/src/Avalonia.Base/Rendering/IPresentationSource.cs new file mode 100644 index 0000000000..bf5beebe49 --- /dev/null +++ b/src/Avalonia.Base/Rendering/IPresentationSource.cs @@ -0,0 +1,46 @@ +using System; +using Avalonia.Input; +using Avalonia.Layout; +using Avalonia.Metadata; +using Avalonia.Platform; + +namespace Avalonia.Rendering; + +// This interface serves two purposes: +// 1) User-facing API (public members) +// 2) A way to provide PresentationSource APIs to Avalonia.Base from Avalonia.Controls +// without cyclic references (internal members) +/// +/// Represents the host of the visual tree. On desktop platforms this is typically backed by a native window. +/// +[NotClientImplementable] +public interface IPresentationSource +{ + /// + /// The current root of the visual tree + /// + public Visual? RootVisual { get; } + + /// + /// The scaling factor to use in rendering. + /// + public double RenderScaling { get; } + + internal IPlatformSettings? PlatformSettings { get; } + + internal IRenderer Renderer { get; } + + internal IHitTester HitTester { get; } + + internal IInputRoot InputRoot { get; } + + internal ILayoutRoot LayoutRoot { get; } + + /// + /// Gets the client size of the window. + /// + internal Size ClientSize { get; } + + internal PixelPoint PointToScreen(Point point); + internal Point PointToClient(PixelPoint point); +} diff --git a/src/Avalonia.Base/Rendering/IRenderLoop.cs b/src/Avalonia.Base/Rendering/IRenderLoop.cs index bf2c221b03..e887832ebc 100644 --- a/src/Avalonia.Base/Rendering/IRenderLoop.cs +++ b/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. /// - [NotClientImplementable] - internal interface IRenderLoop + [PrivateApi] + public interface IRenderLoop { /// /// 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. /// - void Add(IRenderLoopTask i); + internal void Add(IRenderLoopTask i); /// /// Removes an update task. /// /// The update task. - void Remove(IRenderLoopTask i); + internal void Remove(IRenderLoopTask i); /// /// Indicates if the rendering is done on a non-UI thread. /// - bool RunsInBackground { get; } + internal bool RunsInBackground { get; } + + /// + /// Wakes up the render loop to schedule the next tick. + /// Thread-safe: can be called from any thread. + /// + internal void Wakeup(); } } diff --git a/src/Avalonia.Base/Rendering/IRenderLoopTask.cs b/src/Avalonia.Base/Rendering/IRenderLoopTask.cs index f63855e651..67416cc155 100644 --- a/src/Avalonia.Base/Rendering/IRenderLoopTask.cs +++ b/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(); } } diff --git a/src/Avalonia.Base/Rendering/IRenderRoot.cs b/src/Avalonia.Base/Rendering/IRenderRoot.cs deleted file mode 100644 index 820840afbc..0000000000 --- a/src/Avalonia.Base/Rendering/IRenderRoot.cs +++ /dev/null @@ -1,42 +0,0 @@ -using Avalonia.Metadata; - -namespace Avalonia.Rendering -{ - /// - /// Represents the root of a renderable tree. - /// - [NotClientImplementable] - public interface IRenderRoot - { - /// - /// Gets the client size of the window. - /// - Size ClientSize { get; } - - /// - /// Gets the renderer for the window. - /// - public IRenderer Renderer { get; } - - public IHitTester HitTester { get; } - - /// - /// The scaling factor to use in rendering. - /// - double RenderScaling { get; } - - /// - /// Converts a point from screen to client coordinates. - /// - /// The point in screen device coordinates. - /// The point in client coordinates. - Point PointToClient(PixelPoint point); - - /// - /// Converts a point from client to screen coordinates. - /// - /// The point in client coordinates. - /// The point in screen device coordinates. - PixelPoint PointToScreen(Point point); - } -} diff --git a/src/Avalonia.Base/Rendering/IRenderTimer.cs b/src/Avalonia.Base/Rendering/IRenderTimer.cs index 396e84d492..772dcf7656 100644 --- a/src/Avalonia.Base/Rendering/IRenderTimer.cs +++ b/src/Avalonia.Base/Rendering/IRenderTimer.cs @@ -10,16 +10,19 @@ namespace Avalonia.Rendering public interface IRenderTimer { /// - /// 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 /// /// - /// 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 /// - event Action Tick; + Action? Tick { get; set; } /// - /// Indicates if the timer ticks on a non-UI thread + /// Indicates if the timer ticks on a non-UI thread. /// bool RunsInBackground { get; } } diff --git a/src/Avalonia.Base/Rendering/IRenderer.cs b/src/Avalonia.Base/Rendering/IRenderer.cs index 0ca4459c97..56007c7e8c 100644 --- a/src/Avalonia.Base/Rendering/IRenderer.cs +++ b/src/Avalonia.Base/Rendering/IRenderer.cs @@ -10,7 +10,7 @@ namespace Avalonia.Rendering /// Defines the interface for a renderer. /// [PrivateApi] - public interface IRenderer : IDisposable + internal interface IRenderer : IDisposable { /// /// Gets a value indicating whether the renderer should draw specific diagnostics. @@ -75,7 +75,7 @@ namespace Avalonia.Rendering } [PrivateApi] - public interface IHitTester + internal interface IHitTester { /// /// Hit tests a location to find the visuals at the specified point. diff --git a/src/Avalonia.Base/Rendering/PlatformRenderInterfaceContextManager.cs b/src/Avalonia.Base/Rendering/PlatformRenderInterfaceContextManager.cs index db61ad84f1..980c7818c7 100644 --- a/src/Avalonia.Base/Rendering/PlatformRenderInterfaceContextManager.cs +++ b/src/Avalonia.Base/Rendering/PlatformRenderInterfaceContextManager.cs @@ -2,6 +2,7 @@ using System; using System.Collections.Generic; using Avalonia.Metadata; using Avalonia.Platform; +using Avalonia.Platform.Surfaces; using Avalonia.Reactive; namespace Avalonia.Rendering; @@ -77,9 +78,16 @@ internal class PlatformRenderInterfaceContextManager return Disposable.Empty; } - public IRenderTarget CreateRenderTarget(IEnumerable surfaces) + public IRenderTarget CreateRenderTarget(IEnumerable surfaces) { EnsureValidBackendContext(); return _backend!.CreateRenderTarget(surfaces); } + + public bool IsReadyToCreateRenderTarget(IEnumerable surfaces) + { + if (_backend == null) + return IsReady; + return _backend.IsReadyToCreateRenderTarget(surfaces); + } } diff --git a/src/Avalonia.Base/Rendering/RenderLoop.cs b/src/Avalonia.Base/Rendering/RenderLoop.cs index 846cce7a23..9af9c54443 100644 --- a/src/Avalonia.Base/Rendering/RenderLoop.cs +++ b/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 { /// - /// The application render loop. + /// Provides factory methods for creating instances. + /// + [PrivateApi] + public static class RenderLoop + { + /// + /// Creates an from an . + /// + public static IRenderLoop FromTimer(IRenderTimer timer) => new DefaultRenderLoop(timer); + } + + /// + /// Default implementation of the application render loop. /// /// /// 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 + /// 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. /// - internal class RenderLoop : IRenderLoop + internal class DefaultRenderLoop : IRenderLoop { private readonly List _items = new List(); private readonly List _itemsCopy = new List(); - private IRenderTimer? _timer; + private Action _tick; + private readonly IRenderTimer _timer; + private readonly object _timerLock = new(); private int _inTick; - - public static IRenderLoop LocatorAutoInstance - { - get - { - var loop = AvaloniaLocator.Current.GetService(); - if (loop == null) - { - var timer = AvaloniaLocator.Current.GetRequiredService(); - AvaloniaLocator.CurrentMutable.Bind() - .ToConstant(loop = new RenderLoop(timer)); - } - - return loop; - } - } + private volatile bool _hasItems; + private bool _running; + private bool _wakeupPending; /// - /// Initializes a new instance of the class. + /// Initializes a new instance of the class. /// /// The render timer. - public RenderLoop(IRenderTimer timer) + public DefaultRenderLoop(IRenderTimer timer) { _timer = timer; - } - - /// - /// Gets the render timer. - /// - protected IRenderTimer Timer - { - get - { - return _timer ??= AvaloniaLocator.Current.GetRequiredService(); - } + _tick = TimerTick; } /// @@ -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; + } } } } /// - public bool RunsInBackground => Timer.RunsInBackground; + public bool RunsInBackground => _timer.RunsInBackground; + + /// + 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) { diff --git a/src/Avalonia.Base/Rendering/SceneInvalidatedEventArgs.cs b/src/Avalonia.Base/Rendering/SceneInvalidatedEventArgs.cs index 552ecb14ff..86fbae8158 100644 --- a/src/Avalonia.Base/Rendering/SceneInvalidatedEventArgs.cs +++ b/src/Avalonia.Base/Rendering/SceneInvalidatedEventArgs.cs @@ -12,13 +12,9 @@ namespace Avalonia.Rendering /// /// Initializes a new instance of the class. /// - /// The render root that has been updated. /// The updated area. - public SceneInvalidatedEventArgs( - IRenderRoot root, - Rect dirtyRect) + public SceneInvalidatedEventArgs(Rect dirtyRect) { - RenderRoot = root; DirtyRect = dirtyRect; } @@ -27,9 +23,5 @@ namespace Avalonia.Rendering /// public Rect DirtyRect { get; } - /// - /// Gets the render root that has been invalidated. - /// - public IRenderRoot RenderRoot { get; } } } diff --git a/src/Avalonia.Base/Rendering/SleepLoopRenderTimer.cs b/src/Avalonia.Base/Rendering/SleepLoopRenderTimer.cs index 3ad4ea94d0..570dc4cb30 100644 --- a/src/Avalonia.Base/Rendering/SleepLoopRenderTimer.cs +++ b/src/Avalonia.Base/Rendering/SleepLoopRenderTimer.cs @@ -8,10 +8,10 @@ namespace Avalonia.Rendering [PrivateApi] public class SleepLoopRenderTimer : IRenderTimer { - private Action? _tick; - private int _count; - private readonly object _lock = new object(); - private bool _running; + private volatile Action? _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 Tick + + public Action? 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); - } } - - } } diff --git a/src/Avalonia.Base/Rendering/ThreadProxyRenderTimer.cs b/src/Avalonia.Base/Rendering/ThreadProxyRenderTimer.cs index 0f3387cd1a..d15d3a052e 100644 --- a/src/Avalonia.Base/Rendering/ThreadProxyRenderTimer.cs +++ b/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? _tick; - private int _subscriberCount; + private readonly object _lock = new(); + private volatile Action? _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 Tick + public Action? 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; } diff --git a/src/Avalonia.Base/StyledElement.cs b/src/Avalonia.Base/StyledElement.cs index d1b960390c..f511d1de2a 100644 --- a/src/Avalonia.Base/StyledElement.cs +++ b/src/Avalonia.Base/StyledElement.cs @@ -76,7 +76,7 @@ namespace Avalonia public static readonly StyledProperty ThemeProperty = AvaloniaProperty.Register(nameof(Theme)); - private static readonly ControlTheme s_invalidTheme = new ControlTheme(); + [ThreadStatic] private static ControlTheme? s_invalidTheme; private int _initCount; private string? _name; private Classes? _classes; @@ -332,6 +332,9 @@ namespace Avalonia /// IStyleHost? IStyleHost.StylingParent => (IStyleHost?)InheritanceParent; + internal static ControlTheme InvalidTheme + => s_invalidTheme ??= new(); + /// public virtual void BeginInit() { @@ -666,10 +669,10 @@ namespace Avalonia if (this.TryFindResource(key, out var value) && value is ControlTheme t) _implicitTheme = t; else - _implicitTheme = s_invalidTheme; + _implicitTheme = InvalidTheme; } - if (_implicitTheme != s_invalidTheme) + if (_implicitTheme != InvalidTheme) return _implicitTheme; return null; @@ -828,11 +831,11 @@ namespace Avalonia return; // Refetch the implicit theme. - var oldImplicitTheme = _implicitTheme == s_invalidTheme ? null : _implicitTheme; + var oldImplicitTheme = _implicitTheme == InvalidTheme ? null : _implicitTheme; _implicitTheme = null; GetEffectiveTheme(); - var newImplicitTheme = _implicitTheme == s_invalidTheme ? null : _implicitTheme; + var newImplicitTheme = _implicitTheme == InvalidTheme ? null : _implicitTheme; // If the implicit theme has changed, detach the existing theme. if (newImplicitTheme != oldImplicitTheme) diff --git a/src/Avalonia.Base/Threading/AvaloniaSynchronizationContext.cs b/src/Avalonia.Base/Threading/AvaloniaSynchronizationContext.cs index a643445e99..5639963e9e 100644 --- a/src/Avalonia.Base/Threading/AvaloniaSynchronizationContext.cs +++ b/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 ( diff --git a/src/Avalonia.Base/Threading/CulturePreservingExecutionContext.cs b/src/Avalonia.Base/Threading/CulturePreservingExecutionContext.cs deleted file mode 100644 index ec0ebaa4a6..0000000000 --- a/src/Avalonia.Base/Threading/CulturePreservingExecutionContext.cs +++ /dev/null @@ -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; - -/// -/// 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. -/// -internal sealed class CulturePreservingExecutionContext -{ - private readonly ExecutionContext _context; - private CultureAndContext? _cultureAndContext; - - private CulturePreservingExecutionContext(ExecutionContext context) - { - _context = context; - } - - /// - /// Captures the current ExecutionContext and culture information. - /// - /// A new CulturePreservingExecutionContext instance, or null if no context needs to be captured. - 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); - } - - /// - /// Runs the specified callback in the captured execution context while preserving culture information. - /// This method is used for .NET Framework and earlier .NET versions. - /// - /// The execution context to run in. - /// The callback to execute. - /// The state to pass to the callback. - 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; - - /// - /// Executes the callback and saves culture values immediately afterwards. - /// - /// Contains the actual callback and state. - 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(); - } - } - - /// - /// Helper class to manage culture information across execution contexts. - /// - 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 diff --git a/src/Avalonia.Base/Threading/Dispatcher.Invoke.cs b/src/Avalonia.Base/Threading/Dispatcher.Invoke.cs index fbd96ed7d8..90167fa8a1 100644 --- a/src/Avalonia.Base/Threading/Dispatcher.Invoke.cs +++ b/src/Avalonia.Base/Threading/Dispatcher.Invoke.cs @@ -737,9 +737,6 @@ public partial class Dispatcher /// public static DispatcherPriorityAwaitable Yield(DispatcherPriority priority) { - // TODO12: Update to use Dispatcher.CurrentDispatcher once multi-dispatcher support is merged - var current = UIThread; - current.VerifyAccess(); - return UIThread.Resume(priority); + return CurrentDispatcher.Resume(priority); } } diff --git a/src/Avalonia.Base/Threading/Dispatcher.Queue.cs b/src/Avalonia.Base/Threading/Dispatcher.Queue.cs index 954183ffcc..09dd9f27ec 100644 --- a/src/Avalonia.Base/Threading/Dispatcher.Queue.cs +++ b/src/Avalonia.Base/Threading/Dispatcher.Queue.cs @@ -12,7 +12,7 @@ public partial class Dispatcher private bool _explicitBackgroundProcessingRequested; private const int MaximumInputStarvationTimeInFallbackMode = 50; private const int MaximumInputStarvationTimeInExplicitProcessingExplicitMode = 50; - private readonly int _maximumInputStarvationTime; + private int _maximumInputStarvationTime; void RequestBackgroundProcessing() { @@ -101,9 +101,9 @@ public partial class Dispatcher internal static void ResetBeforeUnitTests() { - s_uiThread = null; + ResetGlobalState(); } - + internal static void ResetForUnitTests() { if (s_uiThread == null) @@ -122,14 +122,14 @@ public partial class Dispatcher if (job == null || job.Priority <= DispatcherPriority.Inactive) { s_uiThread.ShutdownImpl(); - s_uiThread = null; + ResetGlobalState(); return; } s_uiThread.ExecuteJob(job); } - } + private void ExecuteJob(DispatcherOperation job) { diff --git a/src/Avalonia.Base/Threading/Dispatcher.ThreadStorage.cs b/src/Avalonia.Base/Threading/Dispatcher.ThreadStorage.cs new file mode 100644 index 0000000000..7d9d0b39cf --- /dev/null +++ b/src/Avalonia.Base/Threading/Dispatcher.ThreadStorage.cs @@ -0,0 +1,95 @@ +using System; +using System.Runtime.CompilerServices; +using System.Threading; +using Avalonia.Controls.Platform; +using Avalonia.Metadata; +using Avalonia.Platform; +using Avalonia.Utilities; + +namespace Avalonia.Threading; + +public partial class Dispatcher +{ + [ThreadStatic] + private static DispatcherReferenceStorage? s_currentThreadDispatcher; + private static readonly object s_globalLock = new(); + private static readonly ConditionalWeakTable s_dispatchers = new(); + + private static Dispatcher? s_uiThread; + + // This class is needed PURELY for ResetForUnitTests, so we can reset s_currentThreadDispatcher for all threads + class DispatcherReferenceStorage + { + public WeakReference Reference = new(null!); + } + + public static Dispatcher CurrentDispatcher + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + get + { + if (s_currentThreadDispatcher?.Reference.TryGetTarget(out var dispatcher) == true) + return dispatcher; + + return new Dispatcher(null); + } + } + + public static Dispatcher? FromThread(Thread thread) + { + lock (s_globalLock) + { + if (s_dispatchers.TryGetValue(thread, out var reference) && reference.Reference.TryGetTarget(out var dispatcher) == true) + return dispatcher; + return null; + } + } + + public static Dispatcher UIThread + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + get + { + static Dispatcher GetUIThreadDispatcherSlow() + { + lock (s_globalLock) + { + return s_uiThread ?? CurrentDispatcher; + } + } + return s_uiThread ?? GetUIThreadDispatcherSlow(); + } + } + + internal static Dispatcher? TryGetUIThread() + { + lock (s_globalLock) + return s_uiThread; + } + + [PrivateApi] + public static void InitializeUIThreadDispatcher(IPlatformThreadingInterface impl) => + InitializeUIThreadDispatcher(new LegacyDispatcherImpl(impl)); + + [PrivateApi] + public static void InitializeUIThreadDispatcher(IDispatcherImpl impl) + { + UIThread.VerifyAccess(); + if (UIThread._initialized) + throw new InvalidOperationException("UI thread dispatcher is already initialized"); + UIThread.ReplaceImplementation(impl); + } + + private static void ResetGlobalState() + { + lock (s_globalLock) + { + foreach (var store in s_dispatchers) + store.Value.Reference = new(null!); + s_dispatchers.Clear(); + + s_currentThreadDispatcher = null; + s_uiThread = null; + } + } +} diff --git a/src/Avalonia.Base/Threading/Dispatcher.Timers.cs b/src/Avalonia.Base/Threading/Dispatcher.Timers.cs index ce16820286..9966a156d2 100644 --- a/src/Avalonia.Base/Threading/Dispatcher.Timers.cs +++ b/src/Avalonia.Base/Threading/Dispatcher.Timers.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Diagnostics; using System.Linq; namespace Avalonia.Threading; @@ -15,7 +16,8 @@ public partial class Dispatcher private long? _dueTimeForBackgroundProcessing; private long? _osTimerSetTo; - internal long Now => _impl.Now; + private readonly Func _timeProvider; + internal long Now => _timeProvider(); private void UpdateOSTimer() { @@ -26,6 +28,7 @@ public partial class Dispatcher _dueTimeForTimers ?? _dueTimeForBackgroundProcessing; if (_osTimerSetTo == nextDueTime) return; + _impl.UpdateTimer(_osTimerSetTo = nextDueTime); } @@ -114,7 +117,8 @@ public partial class Dispatcher bool needToProcessQueue = false; lock (InstanceLock) { - _impl.UpdateTimer(_osTimerSetTo = null); + _impl.UpdateTimer(null); + _osTimerSetTo = null; needToPromoteTimers = _dueTimeForTimers.HasValue && _dueTimeForTimers.Value <= Now; if (needToPromoteTimers) _dueTimeForTimers = null; diff --git a/src/Avalonia.Base/Threading/Dispatcher.cs b/src/Avalonia.Base/Threading/Dispatcher.cs index 8253c2fed2..07582ac3f4 100644 --- a/src/Avalonia.Base/Threading/Dispatcher.cs +++ b/src/Avalonia.Base/Threading/Dispatcher.cs @@ -3,7 +3,10 @@ using System.Diagnostics; using System.Diagnostics.CodeAnalysis; using System.Runtime.CompilerServices; using System.Threading; +using Avalonia.Controls.Platform; +using Avalonia.Metadata; using Avalonia.Platform; +using Avalonia.Utilities; namespace Avalonia.Threading; @@ -17,63 +20,60 @@ namespace Avalonia.Threading; public partial class Dispatcher : IDispatcher { private IDispatcherImpl _impl; + private bool _initialized; internal object InstanceLock { get; } = new(); private IControlledDispatcherImpl? _controlledImpl; - private static Dispatcher? s_uiThread; private IDispatcherImplWithPendingInput? _pendingInputImpl; - private readonly IDispatcherImplWithExplicitBackgroundProcessing? _backgroundProcessingImpl; + private IDispatcherImplWithExplicitBackgroundProcessing? _backgroundProcessingImpl; + private readonly Thread _thread; private readonly AvaloniaSynchronizationContext?[] _priorityContexts = new AvaloniaSynchronizationContext?[DispatcherPriority.MaxValue - DispatcherPriority.MinValue + 1]; - internal Dispatcher(IDispatcherImpl impl) + internal Dispatcher(IDispatcherImpl? impl) { - _impl = impl; - impl.Timer += OnOSTimer; - impl.Signaled += Signaled; - _controlledImpl = _impl as IControlledDispatcherImpl; - _pendingInputImpl = _impl as IDispatcherImplWithPendingInput; - _backgroundProcessingImpl = _impl as IDispatcherImplWithExplicitBackgroundProcessing; - _maximumInputStarvationTime = _backgroundProcessingImpl == null ? - MaximumInputStarvationTimeInFallbackMode : - MaximumInputStarvationTimeInExplicitProcessingExplicitMode; - if (_backgroundProcessingImpl != null) - _backgroundProcessingImpl.ReadyForBackgroundProcessing += OnReadyForExplicitBackgroundProcessing; - - _unhandledExceptionEventArgs = new DispatcherUnhandledExceptionEventArgs(this); - _exceptionFilterEventArgs = new DispatcherUnhandledExceptionFilterEventArgs(this); - } - - public static Dispatcher UIThread - { - [MethodImpl(MethodImplOptions.AggressiveInlining)] - get +#if DEBUG + if (AvaloniaLocator.Current.GetService() != null + || AvaloniaLocator.Current.GetService() != null) + throw new InvalidOperationException( + "Registering IDispatcherImpl or IPlatformThreadingInterface via locator is no longer valid"); +#endif + lock (s_globalLock) { - return s_uiThread ??= CreateUIThreadDispatcher(); - } - } + _thread = Thread.CurrentThread; + if (FromThread(_thread) != null) + throw new InvalidOperationException("The current thread already has a dispatcher"); - public bool SupportsRunLoops => _controlledImpl != null; + // The first created dispatcher becomes "UI thread one" + s_uiThread ??= this; - [MethodImpl(MethodImplOptions.NoInlining)] - private static Dispatcher CreateUIThreadDispatcher() - { - var impl = AvaloniaLocator.Current.GetService(); - if (impl == null) + s_dispatchers.Remove(Thread.CurrentThread); + s_dispatchers.Add(Thread.CurrentThread, + s_currentThreadDispatcher = new() { Reference = new WeakReference(this) }); + } + + if (impl is null) { - var platformThreading = AvaloniaLocator.Current.GetService(); - if (platformThreading != null) - impl = new LegacyDispatcherImpl(platformThreading); - else - impl = new NullDispatcherImpl(); + var st = Stopwatch.StartNew(); + _timeProvider = () => st.ElapsedMilliseconds; } - return new Dispatcher(impl); + else + _timeProvider = () => impl.Now; + + _impl = null!; // Set by ReplaceImplementation + ReplaceImplementation(impl); + + + _unhandledExceptionEventArgs = new DispatcherUnhandledExceptionEventArgs(this); + _exceptionFilterEventArgs = new DispatcherUnhandledExceptionFilterEventArgs(this); } + public bool SupportsRunLoops => _controlledImpl != null; + /// /// Checks that the current thread is the UI thread. /// - public bool CheckAccess() => _impl.CurrentThreadIsLoopThread; + public bool CheckAccess() => Thread.CurrentThread == _thread; /// /// Checks that the current thread is the UI thread and throws if not. @@ -89,15 +89,64 @@ public partial class Dispatcher : IDispatcher [DoesNotReturn] [MethodImpl(MethodImplOptions.NoInlining)] static void ThrowVerifyAccess() - => throw new InvalidOperationException("Call from invalid thread"); + => throw new InvalidOperationException("The calling thread cannot access this object because a different thread owns it."); ThrowVerifyAccess(); } } + public Thread Thread => _thread; + internal AvaloniaSynchronizationContext GetContextWithPriority(DispatcherPriority priority) { DispatcherPriority.Validate(priority, nameof(priority)); var index = priority - DispatcherPriority.MinValue; return _priorityContexts[index] ??= new(this, priority); } + + [PrivateApi] + public IDispatcherImpl PlatformImpl => _impl; + + private void ReplaceImplementation(IDispatcherImpl? impl) + { + // TODO: Consider moving the helper out of Avalonia.Win32 so + // it's usable earlier + using var _ = NonPumpingLockHelper.Use(); + + + if (impl?.CurrentThreadIsLoopThread == false) + throw new InvalidOperationException("IDispatcherImpl belongs to a different thread"); + + if (_impl != null!) // Null in ctor + { + _impl.Timer -= OnOSTimer; + _impl.Signaled -= Signaled; + if (_backgroundProcessingImpl != null) + _backgroundProcessingImpl.ReadyForBackgroundProcessing -= OnReadyForExplicitBackgroundProcessing; + _impl = null!; + _controlledImpl = null; + _pendingInputImpl = null; + _backgroundProcessingImpl = null; + } + + if (impl != null) + _initialized = true; + else + impl = new ManagedDispatcherImpl(null); + _impl = impl; + + impl.Timer += OnOSTimer; + impl.Signaled += Signaled; + _controlledImpl = _impl as IControlledDispatcherImpl; + _pendingInputImpl = _impl as IDispatcherImplWithPendingInput; + _backgroundProcessingImpl = _impl as IDispatcherImplWithExplicitBackgroundProcessing; + _maximumInputStarvationTime = _backgroundProcessingImpl == null ? + MaximumInputStarvationTimeInFallbackMode : + MaximumInputStarvationTimeInExplicitProcessingExplicitMode; + if (_backgroundProcessingImpl != null) + _backgroundProcessingImpl.ReadyForBackgroundProcessing += OnReadyForExplicitBackgroundProcessing; + if (_signaled) + _impl.Signal(); + _osTimerSetTo = null; + UpdateOSTimer(); + } } diff --git a/src/Avalonia.Base/Threading/DispatcherOperation.cs b/src/Avalonia.Base/Threading/DispatcherOperation.cs index 3a4513652e..ea48fa31b0 100644 --- a/src/Avalonia.Base/Threading/DispatcherOperation.cs +++ b/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 { diff --git a/src/Avalonia.Base/Threading/IDispatcherImpl.cs b/src/Avalonia.Base/Threading/IDispatcherImpl.cs index dd438b176e..f8d5cb8947 100644 --- a/src/Avalonia.Base/Threading/IDispatcherImpl.cs +++ b/src/Avalonia.Base/Threading/IDispatcherImpl.cs @@ -80,33 +80,4 @@ internal class LegacyDispatcherImpl : IDispatcherImpl _timer = null; Timer?.Invoke(); } -} - -internal sealed class NullDispatcherImpl : IDispatcherImpl -{ - public bool CurrentThreadIsLoopThread => true; - - public void Signal() - { - - } - - public event Action? Signaled - { - add { } - remove { } - } - - public event Action? Timer - { - add { } - remove { } - } - - public long Now => 0; - - public void UpdateTimer(long? dueTimeInMs) - { - - } -} +} \ No newline at end of file diff --git a/src/Avalonia.Base/Threading/NonPumpingSyncContext.cs b/src/Avalonia.Base/Threading/NonPumpingSyncContext.cs index 03fc0cc76c..3c12d693eb 100644 --- a/src/Avalonia.Base/Threading/NonPumpingSyncContext.cs +++ b/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); diff --git a/src/Avalonia.Base/Utilities/ArrayBuilder.cs b/src/Avalonia.Base/Utilities/ArrayBuilder.cs index bbbcc39ecc..c12a4906f3 100644 --- a/src/Avalonia.Base/Utilities/ArrayBuilder.cs +++ b/src/Avalonia.Base/Utilities/ArrayBuilder.cs @@ -136,7 +136,6 @@ namespace Avalonia.Utilities /// public void Clear() { -#if NET6_0_OR_GREATER if (RuntimeHelpers.IsReferenceOrContainsReferences()) { ClearArray(); @@ -145,9 +144,6 @@ namespace Avalonia.Utilities { _size = 0; } -#else - ClearArray(); -#endif } private void ClearArray() diff --git a/src/Avalonia.Base/Utilities/AvaloniaPropertyDictionary.cs b/src/Avalonia.Base/Utilities/AvaloniaPropertyDictionary.cs index ab34e85220..13272c7e7d 100644 --- a/src/Avalonia.Base/Utilities/AvaloniaPropertyDictionary.cs +++ b/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); diff --git a/src/Avalonia.Base/Utilities/EnumHelper.cs b/src/Avalonia.Base/Utilities/EnumHelper.cs deleted file mode 100644 index fd9176985e..0000000000 --- a/src/Avalonia.Base/Utilities/EnumHelper.cs +++ /dev/null @@ -1,34 +0,0 @@ -using System; - -namespace Avalonia.Utilities -{ - internal class EnumHelper - { -#if NET6_0_OR_GREATER - public static T Parse(ReadOnlySpan key, bool ignoreCase) where T : struct - { - return Enum.Parse(key, ignoreCase); - } - - public static bool TryParse(ReadOnlySpan key, bool ignoreCase, out T result) where T : struct - { - return Enum.TryParse(key, ignoreCase, out result); - } -#else - public static T Parse(string key, bool ignoreCase) where T : struct - { - return (T)Enum.Parse(typeof(T), key, ignoreCase); - } - - public static bool TryParse(string key, bool ignoreCase, out T result) where T : struct - { - return Enum.TryParse(key, ignoreCase, out result); - } - - public static bool TryParse(ReadOnlySpan key, bool ignoreCase, out T result) where T : struct - { - return Enum.TryParse(key.ToString(), ignoreCase, out result); - } -#endif - } -} diff --git a/src/Avalonia.Base/Utilities/MathUtilities.cs b/src/Avalonia.Base/Utilities/MathUtilities.cs index 1ebf4f6457..2c18836d75 100644 --- a/src/Avalonia.Base/Utilities/MathUtilities.cs +++ b/src/Avalonia.Base/Utilities/MathUtilities.cs @@ -1,19 +1,12 @@ using System; using System.Runtime.CompilerServices; -#if !BUILDTASK -using Avalonia.Metadata; -#endif namespace Avalonia.Utilities { /// /// Provides math utilities not provided in System.Math. /// -#if !BUILDTASK - [Unstable("This API might be removed in next major version. Please use corresponding BCL APIs.")] - public -#endif - static class MathUtilities + internal static class MathUtilities { // smallest such that 1.0+DoubleEpsilon != 1.0 internal const double DoubleEpsilon = 2.2204460492503131e-016; diff --git a/src/Avalonia.Base/Utilities/Polyfills.cs b/src/Avalonia.Base/Utilities/Polyfills.cs deleted file mode 100644 index 9ee72ab112..0000000000 --- a/src/Avalonia.Base/Utilities/Polyfills.cs +++ /dev/null @@ -1,43 +0,0 @@ -using System.Collections.Generic; -using System.Diagnostics.CodeAnalysis; - -internal static class Polyfills -{ - #if !NET6_0_OR_GREATER - - public static bool TryDequeue(this Queue queue, [MaybeNullWhen(false)]out T item) - { - if (queue.Count == 0) - { - item = default; - return false; - } - - item = queue.Dequeue(); - return true; - } - - #endif -} - -#if !NET7_0_OR_GREATER - -namespace System.Diagnostics.CodeAnalysis -{ - [System.AttributeUsage( - System.AttributeTargets.Method | System.AttributeTargets.Parameter | System.AttributeTargets.Property, - AllowMultiple = false, Inherited = false)] - internal sealed class UnscopedRefAttribute : Attribute - { - } - - struct S - { - int _field; - - // Okay: `field` has the ref-safe-to-escape of `this` which is *calling method* because - // it is a `ref` - [UnscopedRef] ref int Prop1 => ref _field; - } -} -#endif diff --git a/src/Avalonia.Base/Utilities/RefCountingSmallDictionary.cs b/src/Avalonia.Base/Utilities/RefCountingSmallDictionary.cs index 86c9fd7ba1..64838a845f 100644 --- a/src/Avalonia.Base/Utilities/RefCountingSmallDictionary.cs +++ b/src/Avalonia.Base/Utilities/RefCountingSmallDictionary.cs @@ -10,20 +10,13 @@ internal struct RefCountingSmallDictionary : IEnumerable : IEnumerable : IEnumerable> IEnumerable>.GetEnumerator() => GetEnumerator(); IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); -} \ No newline at end of file +} diff --git a/src/Avalonia.Base/Utilities/SingleOrDictionary.cs b/src/Avalonia.Base/Utilities/SingleOrDictionary.cs deleted file mode 100644 index 068c73ba33..0000000000 --- a/src/Avalonia.Base/Utilities/SingleOrDictionary.cs +++ /dev/null @@ -1,144 +0,0 @@ -using System; -using System.Collections; -using System.Collections.Generic; -using System.Diagnostics.CodeAnalysis; -using System.Linq; - -namespace Avalonia.Utilities -{ - /// - /// Stores either a single key value pair or constructs a dictionary when more than one value is stored. - /// - /// The type of the key. - /// The type of the value. - internal class SingleOrDictionary : IEnumerable> - where TKey : notnull - { - private KeyValuePair? _singleValue; - private Dictionary? dictionary; - - public void Add(TKey key, TValue value) - { - if (_singleValue != null) - { - dictionary = new Dictionary(); - ((ICollection>)dictionary).Add(_singleValue.Value); - _singleValue = null; - } - - if (dictionary != null) - { - dictionary.Add(key, value); - } - else - { - _singleValue = new KeyValuePair(key, value); - } - } - - public bool TryGetValue(TKey key, [MaybeNullWhen(false)] out TValue value) - { - if (dictionary == null) - { - if (!_singleValue.HasValue || !EqualityComparer.Default.Equals(_singleValue.Value.Key, key)) - { - value = default; - return false; - } - else - { - value = _singleValue.Value.Value; - return true; - } - } - else - { - return dictionary.TryGetValue(key, out value); - } - } - - public IEnumerator> GetEnumerator() - { - if (dictionary == null) - { - if (_singleValue.HasValue) - { - return new SingleEnumerator>(_singleValue.Value); - } - } - else - { - return dictionary.GetEnumerator(); - } - return Enumerable.Empty>().GetEnumerator(); - } - - IEnumerator IEnumerable.GetEnumerator() - { - return GetEnumerator(); - } - - public IEnumerable Values - { - get - { - if(dictionary == null) - { - if (_singleValue.HasValue) - { - return new[] { _singleValue.Value.Value }; - } - } - else - { - return dictionary.Values; - } - return Enumerable.Empty(); - } - } - - private class SingleEnumerator : IEnumerator - { - private readonly T value; - private int index = -1; - - public SingleEnumerator(T value) - { - this.value = value; - } - - public T Current - { - get - { - if (index == 0) - { - return value; - } - else - { - throw new InvalidOperationException(); - } - } - } - - object? IEnumerator.Current => Current; - - public void Dispose() - { - } - - public bool MoveNext() - { - index++; - return index < 1; - } - - public void Reset() - { - index = -1; - } - } - - } -} diff --git a/src/Avalonia.Base/Utilities/SmallDictionary.cs b/src/Avalonia.Base/Utilities/SmallDictionary.cs index bfb3a32bac..99283522af 100644 --- a/src/Avalonia.Base/Utilities/SmallDictionary.cs +++ b/src/Avalonia.Base/Utilities/SmallDictionary.cs @@ -177,7 +177,6 @@ internal struct InlineDictionary : IEnumerable : IEnumerable s.Invalidated -= h); private Rect _bounds; - private IRenderRoot? _visualRoot; + internal IPresentationSource? PresentationSource { get; private set; } private Visual? _visualParent; private bool _hasMirrorTransform; private TargetWeakEventSubscriber? _affectsRenderWeakSubscriber; @@ -154,8 +156,6 @@ namespace Avalonia /// public Visual() { - _visualRoot = this as IRenderRoot; - // Disable transitions until we're added to the visual tree. DisableTransitions(); @@ -208,6 +208,11 @@ namespace Avalonia /// public bool IsEffectivelyVisible { get; private set; } = true; + /// + /// Raised when changes. + /// + internal event EventHandler? IsEffectivelyVisibleChanged; + /// /// Updates the property based on the parent's /// . @@ -221,6 +226,7 @@ namespace Avalonia return; IsEffectivelyVisible = isEffectivelyVisible; + IsEffectivelyVisibleChanged?.Invoke(this, EventArgs.Empty); // PERF-SENSITIVE: This is called on entire hierarchy and using foreach or LINQ // will cause extra allocations and overhead. @@ -339,7 +345,9 @@ namespace Avalonia /// /// Gets the root of the visual tree, if the control is attached to a visual tree. /// - protected internal IRenderRoot? VisualRoot => _visualRoot; + protected internal Visual? VisualRoot => PresentationSource?.RootVisual; + + internal IInputRoot? GetInputRoot() => PresentationSource?.InputRoot; internal RenderOptions RenderOptions { @@ -366,7 +374,7 @@ namespace Avalonia /// /// Gets a value indicating whether this control is attached to a visual root. /// - internal bool IsAttachedToVisualTree => VisualRoot != null; + internal bool IsAttachedToVisualTree => this.PresentationSource != null; /// /// Gets the control's parent visual. @@ -409,7 +417,7 @@ namespace Avalonia /// public void InvalidateVisual() { - VisualRoot?.Renderer.AddDirty(this); + PresentationSource?.Renderer.AddDirty(this); } /// @@ -514,7 +522,7 @@ namespace Avalonia protected override void LogicalChildrenCollectionChanged(object? sender, NotifyCollectionChangedEventArgs e) { base.LogicalChildrenCollectionChanged(sender, e); - VisualRoot?.Renderer.RecalculateChildren(this); + PresentationSource?.Renderer.RecalculateChildren(this); } /// @@ -526,12 +534,8 @@ namespace Avalonia { Logger.TryGet(LogEventLevel.Verbose, LogArea.Visual)?.Log(this, "Attached to visual tree"); - _visualRoot = e.Root; + PresentationSource = e.PresentationSource; RootedVisualChildrenCount++; - if (_visualParent is null) - { - throw new InvalidOperationException("Visual was attached to the root without being added to the visual parent first."); - } if (RenderTransform is IMutableTransform mutableTransform) { @@ -539,27 +543,30 @@ namespace Avalonia } EnableTransitions(); - if (_visualRoot.Renderer is IRendererWithCompositor compositingRenderer) + if (PresentationSource.Renderer is IRendererWithCompositor compositingRenderer) { AttachToCompositor(compositingRenderer.Compositor); } InvalidateMirrorTransform(); - UpdateIsEffectivelyVisible(_visualParent.IsEffectivelyVisible); + UpdateIsEffectivelyVisible(_visualParent?.IsEffectivelyVisible ?? true); OnAttachedToVisualTree(e); AttachedToVisualTree?.Invoke(this, e); InvalidateVisual(); - _visualRoot.Renderer.RecalculateChildren(_visualParent); + if (_visualParent != null) + { + PresentationSource.Renderer.RecalculateChildren(_visualParent); - if (ZIndex != 0) - _visualParent.HasNonUniformZIndexChildren = true; + if (ZIndex != 0) + _visualParent.HasNonUniformZIndexChildren = true; + } var visualChildren = VisualChildren; var visualChildrenCount = visualChildren.Count; for (var i = 0; i < visualChildrenCount; i++) { - if (visualChildren[i] is { } child && child._visualRoot != e.Root) // child may already have been attached within an event handler + if (visualChildren[i] is { } child && child.PresentationSource != e.PresentationSource) // child may already have been attached within an event handler { child.OnAttachedToVisualTreeCore(e); } @@ -574,8 +581,7 @@ namespace Avalonia protected virtual void OnDetachedFromVisualTreeCore(VisualTreeAttachmentEventArgs e) { Logger.TryGet(LogEventLevel.Verbose, LogArea.Visual)?.Log(this, "Detached from visual tree"); - - _visualRoot = this as IRenderRoot; + RootedVisualChildrenCount--; if (RenderTransform is IMutableTransform mutableTransform) @@ -589,7 +595,9 @@ namespace Avalonia DetachFromCompositor(); DetachedFromVisualTree?.Invoke(this, e); - e.Root.Renderer.AddDirty(this); + PresentationSource?.Renderer.AddDirty(this); + + PresentationSource = null; var visualChildren = VisualChildren; var visualChildrenCount = visualChildren.Count; @@ -686,7 +694,7 @@ namespace Avalonia parentVisual.HasNonUniformZIndexChildren = true; sender?.InvalidateVisual(); - parent?.VisualRoot?.Renderer.RecalculateChildren(parent); + parent?.PresentationSource?.Renderer.RecalculateChildren(parent); } /// @@ -714,17 +722,15 @@ namespace Avalonia var old = _visualParent; _visualParent = value; - if (_visualRoot is not null && old is not null) + if (PresentationSource is not null && old is not null) { - var e = new VisualTreeAttachmentEventArgs(old, _visualRoot); + var e = new VisualTreeAttachmentEventArgs(old, PresentationSource); OnDetachedFromVisualTreeCore(e); } - if (_visualParent is IRenderRoot || _visualParent?.IsAttachedToVisualTree == true) + if (_visualParent?.IsAttachedToVisualTree == true) { - var root = this.FindAncestorOfType() ?? - throw new AvaloniaInternalException("Visual is atached to visual tree but root could not be found."); - var e = new VisualTreeAttachmentEventArgs(_visualParent, root); + var e = new VisualTreeAttachmentEventArgs(_visualParent, _visualParent.PresentationSource!); OnAttachedToVisualTreeCore(e); } @@ -810,5 +816,26 @@ namespace Avalonia HasMirrorTransform = shouldApplyMirrorTransform; } + + internal void SetPresentationSourceForRootVisual(IPresentationSource? presentationSource) + { + if(presentationSource == PresentationSource) + return; + + if (PresentationSource != null) + { + if (presentationSource != null) + throw new InvalidOperationException( + "Visual is already attached to a presentation source. Only one presentation source can be attached to a visual tree."); + OnDetachedFromVisualTreeCore(new(null, PresentationSource)); + } + + PresentationSource = presentationSource; + if(PresentationSource != null) + { + var e = new VisualTreeAttachmentEventArgs(null, PresentationSource); + OnAttachedToVisualTreeCore(e); + } + } } } diff --git a/src/Avalonia.Base/VisualExtensions.cs b/src/Avalonia.Base/VisualExtensions.cs index e8dc5465d6..3df2eca039 100644 --- a/src/Avalonia.Base/VisualExtensions.cs +++ b/src/Avalonia.Base/VisualExtensions.cs @@ -16,10 +16,11 @@ namespace Avalonia /// The point in client coordinates. public static Point PointToClient(this Visual visual, PixelPoint point) { - var root = visual.VisualRoot ?? - throw new ArgumentException("Control does not belong to a visual tree.", nameof(visual)); - var rootPoint = root.PointToClient(point); - return ((Visual)root).TranslatePoint(rootPoint, visual)!.Value; + var source = visual.PresentationSource; + var root = source?.RootVisual ?? + throw new ArgumentException("Control does not belong to a visual tree.", nameof(visual)); + var rootPoint = source.PointToClient(point); + return root.TranslatePoint(rootPoint, visual)!.Value; } /// @@ -30,10 +31,11 @@ namespace Avalonia /// The point in screen coordinates. public static PixelPoint PointToScreen(this Visual visual, Point point) { - var root = visual.VisualRoot ?? - throw new ArgumentException("Control does not belong to a visual tree.", nameof(visual)); - var p = visual.TranslatePoint(point, (Visual)root); - return root.PointToScreen(p!.Value); + var source = visual.PresentationSource; + var root = source?.RootVisual ?? + throw new ArgumentException("Control does not belong to a visual tree.", nameof(visual)); + var p = visual.TranslatePoint(point, root); + return source.PointToScreen(p!.Value); } /// diff --git a/src/Avalonia.Base/VisualTree/IHostedVisualTreeRoot.cs b/src/Avalonia.Base/VisualTree/IHostedVisualTreeRoot.cs index 9c33b1ffb9..e2a457bdc9 100644 --- a/src/Avalonia.Base/VisualTree/IHostedVisualTreeRoot.cs +++ b/src/Avalonia.Base/VisualTree/IHostedVisualTreeRoot.cs @@ -3,7 +3,7 @@ namespace Avalonia.VisualTree /// /// Interface for controls that are at the root of a hosted visual tree, such as popups. /// - public interface IHostedVisualTreeRoot + internal interface IHostedVisualTreeRoot { /// /// Gets the visual tree host. diff --git a/src/Avalonia.Base/VisualTree/VisualExtensions.cs b/src/Avalonia.Base/VisualTree/VisualExtensions.cs index b202a2e4b7..b97c15c4df 100644 --- a/src/Avalonia.Base/VisualTree/VisualExtensions.cs +++ b/src/Avalonia.Base/VisualTree/VisualExtensions.cs @@ -3,6 +3,8 @@ using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; using System.Linq; using System.Runtime.CompilerServices; +using Avalonia.Layout; +using Avalonia.Platform; using Avalonia.Rendering; using Avalonia.Utilities; @@ -331,9 +333,10 @@ namespace Avalonia.VisualTree { ThrowHelper.ThrowIfNull(visual, nameof(visual)); - var root = visual.GetVisualRoot(); + var source = visual.GetPresentationSource(); + var root = source?.RootVisual; - if (root is null) + if (source is null || root is null) { return null; } @@ -342,7 +345,7 @@ namespace Avalonia.VisualTree if (rootPoint.HasValue) { - return root.HitTester.HitTestFirst(rootPoint.Value, visual, filter); + return source.HitTester.HitTestFirst(rootPoint.Value, visual, filter); } return null; @@ -380,14 +383,14 @@ namespace Avalonia.VisualTree { ThrowHelper.ThrowIfNull(visual, nameof(visual)); - var root = visual.GetVisualRoot(); + var source = visual.GetPresentationSource(); - if (root is null) + if (source is null) { return Array.Empty(); } - return root.HitTester.HitTest(p, visual, filter); + return source.HitTester.HitTest(p, visual, filter); } /// @@ -456,19 +459,26 @@ namespace Avalonia.VisualTree return visual.VisualParent as T; } + + public static IPresentationSource? GetPresentationSource(this Visual visual) => visual.PresentationSource; + + // TODO: Verify all usages, this is no longer necessary a TopLevel + internal static Visual? GetVisualRoot(this Visual visual) => visual.PresentationSource?.RootVisual; + + internal static ILayoutRoot? GetLayoutRoot(this Visual visual) => visual.PresentationSource?.LayoutRoot; + /// - /// Gets the root visual for an . + /// Gets the layout manager for the visual's presentation source, or null if the visual is not attached to a visual root. /// - /// The visual. - /// - /// The root visual or null if the visual is not rooted. - /// - public static IRenderRoot? GetVisualRoot(this Visual visual) - { - ThrowHelper.ThrowIfNull(visual, nameof(visual)); + public static ILayoutManager? GetLayoutManager(this Visual visual) => + visual.PresentationSource?.LayoutRoot.LayoutManager; - return visual as IRenderRoot ?? visual.VisualRoot; - } + /// + /// Attempts to obtain platform settings from the visual's root. + /// This will return null if the visual is not attached to a visual root. + /// + public static IPlatformSettings? GetPlatformSettings(this Visual visual) => + visual.GetPresentationSource()?.PlatformSettings; /// /// Returns a value indicating whether this control is attached to a visual root. diff --git a/src/Avalonia.Base/VisualTreeAttachmentEventArgs.cs b/src/Avalonia.Base/VisualTreeAttachmentEventArgs.cs index c0e8b1613f..8b644e1b25 100644 --- a/src/Avalonia.Base/VisualTreeAttachmentEventArgs.cs +++ b/src/Avalonia.Base/VisualTreeAttachmentEventArgs.cs @@ -1,4 +1,5 @@ using System; +using System.Diagnostics; using Avalonia.Rendering; namespace Avalonia @@ -12,22 +13,37 @@ namespace Avalonia /// /// Initializes a new instance of the class. /// - /// The parent that the visual is being attached to or detached from. - /// The root visual. - public VisualTreeAttachmentEventArgs(Visual parent, IRenderRoot root) + /// The parent that the visual's tree is being attached to or detached from. + /// Presentation source this visual is being attached to. + public VisualTreeAttachmentEventArgs(Visual? attachmentPoint, IPresentationSource presentationSource) { - Parent = parent ?? throw new ArgumentNullException(nameof(parent)); - Root = root ?? throw new ArgumentNullException(nameof(root)); + RootVisual = presentationSource.RootVisual ?? + throw new InvalidOperationException("PresentationSource must have a non-null RootVisual."); + AttachmentPoint = attachmentPoint; + PresentationSource = presentationSource ?? throw new ArgumentNullException(nameof(presentationSource)); } /// - /// Gets the parent that the visual is being attached to or detached from. + /// Gets the parent that the visual's tree is being attached to or detached from, null means that + /// the entire tree is being attached to a PresentationSource /// - public Visual Parent { get; } + public Visual? AttachmentPoint { get; } + + [Obsolete("Use " + nameof(AttachmentPoint))] + public Visual? Parent => AttachmentPoint; /// /// Gets the root of the visual tree that the visual is being attached to or detached from. /// - public IRenderRoot Root { get; } + public IPresentationSource PresentationSource { get; } + + [Obsolete("This was previously always returning TopLevel. This is no longer guaranteed. Use TopLevel.GetTopLevel(this) if you need a TopLevel or args.RootVisual if you are interested in the root of the visual tree.")] + public Visual Root => RootVisual; + + /// + /// The root visual of the tree this visual is being attached to or detached from. + /// This is guaranteed to be non-null and will be the same as . + /// + public Visual RootVisual { get; set; } } } diff --git a/src/Avalonia.Base/composition-schema.xml b/src/Avalonia.Base/composition-schema.xml index febf465042..defe02769c 100644 --- a/src/Avalonia.Base/composition-schema.xml +++ b/src/Avalonia.Base/composition-schema.xml @@ -22,6 +22,7 @@ + diff --git a/src/Avalonia.Build.Tasks/Avalonia.Build.Tasks.csproj b/src/Avalonia.Build.Tasks/Avalonia.Build.Tasks.csproj index bc6380a012..cf59791653 100644 --- a/src/Avalonia.Build.Tasks/Avalonia.Build.Tasks.csproj +++ b/src/Avalonia.Build.Tasks/Avalonia.Build.Tasks.csproj @@ -129,9 +129,9 @@ - - - + + + diff --git a/src/Avalonia.Controls.ColorPicker/ColorSpectrum/ColorSpectrum.cs b/src/Avalonia.Controls.ColorPicker/ColorSpectrum/ColorSpectrum.cs index 7ddd6d15ec..39cd5f0c5b 100644 --- a/src/Avalonia.Controls.ColorPicker/ColorSpectrum/ColorSpectrum.cs +++ b/src/Avalonia.Controls.ColorPicker/ColorSpectrum/ColorSpectrum.cs @@ -348,7 +348,7 @@ namespace Avalonia.Controls.Primitives } /// - protected override void OnGotFocus(GotFocusEventArgs e) + protected override void OnGotFocus(FocusChangedEventArgs e) { // We only want to bother with the color name tool tip if we can provide color names. if (_selectionEllipsePanel != null && @@ -363,7 +363,7 @@ namespace Avalonia.Controls.Primitives } /// - protected override void OnLostFocus(RoutedEventArgs e) + protected override void OnLostFocus(FocusChangedEventArgs e) { // We only want to bother with the color name tool tip if we can provide color names. if (_selectionEllipsePanel != null && diff --git a/src/Avalonia.Controls.ColorPicker/Helpers/ColorHelper.cs b/src/Avalonia.Controls.ColorPicker/Helpers/ColorHelper.cs index 7d03c1bfd0..bb6d05d82e 100644 --- a/src/Avalonia.Controls.ColorPicker/Helpers/ColorHelper.cs +++ b/src/Avalonia.Controls.ColorPicker/Helpers/ColorHelper.cs @@ -15,12 +15,7 @@ namespace Avalonia.Controls.Primitives private static readonly Dictionary _cachedKnownColorNames = new Dictionary(); private static readonly object _displayNameCacheMutex = new object(); private static readonly object _knownColorCacheMutex = new object(); - private static readonly KnownColor[] _knownColors = -#if NET6_0_OR_GREATER - Enum.GetValues(); -#else - (KnownColor[])Enum.GetValues(typeof(KnownColor)); -#endif + private static readonly KnownColor[] _knownColors = Enum.GetValues(); /// /// Gets the relative (perceptual) luminance/brightness of the given color. diff --git a/src/Avalonia.Controls/Animation/ConnectedAnimation.cs b/src/Avalonia.Controls/Animation/ConnectedAnimation.cs new file mode 100644 index 0000000000..083e0fd604 --- /dev/null +++ b/src/Avalonia.Controls/Animation/ConnectedAnimation.cs @@ -0,0 +1,797 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Threading; +using System.Threading.Tasks; +using Avalonia.Animation.Easings; +using Avalonia.Controls; +using Avalonia.Controls.Presenters; +using Avalonia.Controls.Primitives; +using Avalonia.Layout; +using Avalonia.Media; +using Avalonia.Media.Imaging; +using Avalonia.Styling; +using Avalonia.Logging; +using Avalonia.Threading; +using Avalonia.VisualTree; + +namespace Avalonia.Animation +{ + /// + /// Provides data for the event. + /// + internal sealed class ConnectedAnimationCompletedEventArgs : EventArgs + { + internal ConnectedAnimationCompletedEventArgs(bool cancelled) => Cancelled = cancelled; + + /// + /// Gets a value indicating whether the animation was cancelled before it completed. + /// When the destination element's opacity has already been + /// restored but no visual transition was shown. + /// + public bool Cancelled { get; } + } + + /// + /// Animates an element seamlessly between two views during navigation by flying a + /// proxy over the . + /// + /// + /// + /// Obtain an instance via , + /// then start it with after navigation. + /// + /// + /// The animation auto-disposes after three seconds if not consumed (matching UWP behaviour). + /// + /// + internal class ConnectedAnimation : IDisposable + { + private const double CoordinatedFadeStartThreshold = 0.6; + private const double CoordinatedFadeRange = 0.4; + + private readonly string _key; + private readonly ConnectedAnimationService _service; + + private Rect _sourceBounds; + private CornerRadius _sourceCornerRadius; + private IBrush? _sourceBackground; + private Thickness _sourceBorderThickness; + private IBrush? _sourceBorderBrush; + private RenderTargetBitmap? _sourceSnapshot; + + private bool _isConsumed; + private bool _disposed; + + private CancellationTokenSource? _timeoutCts; + private IDisposable? _timeoutTimerDisposable; + private CancellationTokenSource? _animationCts; + private DispatcherTimer? _animationTimer; + + private static readonly SplineEasing s_directEasing = new(0, 0, 0.58, 1.0); + private static readonly SplineEasing s_basicEasing = new(0.42, 0, 0.58, 1); + private static readonly SplineEasing s_gravityEasing = new(0.1, 0.9, 0.2, 1.0); + + // Active-flight state used by Dispose to clean up if cancelled mid-animation. + private Visual? _activeDestination; + private double _activeDestOriginalOpacity; + private Border? _activeProxy; + private OverlayLayer? _activeOverlayLayer; + + internal ConnectedAnimation(string key, Visual source, ConnectedAnimationService service) + { + _key = key; + _service = service; + + _sourceCornerRadius = GetCornerRadius(source); + _sourceBackground = GetBackground(source); + _sourceBorderThickness = GetBorderThickness(source); + _sourceBorderBrush = GetBorderBrush(source); + + var topLevel = source.FindAncestorOfType(); + if (topLevel != null && source.Bounds.Width > 0 && source.Bounds.Height > 0) + { + var transform = source.TransformToVisual(topLevel); + if (transform.HasValue) + { + _sourceBounds = new Rect( + transform.Value.Transform(new Point(0, 0)), + new Size(source.Bounds.Width, source.Bounds.Height)); + } + + CaptureSnapshot(source, topLevel); + } + + // Auto-dispose after 3 s if not consumed (matches UWP behaviour). + _timeoutCts = new CancellationTokenSource(); + var token = _timeoutCts.Token; + _timeoutTimerDisposable = DispatcherTimer.RunOnce(() => + { + if (!token.IsCancellationRequested && !_isConsumed) + Dispose(); + }, TimeSpan.FromSeconds(3), DispatcherPriority.Background); + } + + /// Gets the key that identifies this animation. + public string Key => _key; + + /// Gets a value indicating whether TryStart has been called. + public bool IsConsumed => _isConsumed; + + /// + /// Gets or sets the configuration that controls timing and visual style. + /// Set this before calling TryStart. + /// + public ConnectedAnimationConfiguration? Configuration { get; set; } + + /// + /// Raised when the animation finishes or is cancelled. + /// Check to distinguish the cases. + /// + public event EventHandler? Completed; + + /// + /// Starts the animation towards . + /// Returns if the animation has already been consumed or disposed. + /// + public bool TryStart(Visual destination) + { + ArgumentNullException.ThrowIfNull(destination); + return TryStart(destination, Array.Empty()); + } + + /// + /// Starts the animation towards with optional + /// that fade in during the last 40 % of the animation. + /// Returns if the animation has already been consumed or disposed. + /// + public bool TryStart(Visual destination, IReadOnlyList coordinatedElements) + { + ArgumentNullException.ThrowIfNull(destination); + ArgumentNullException.ThrowIfNull(coordinatedElements); + if (_isConsumed || _disposed) + return false; + + _isConsumed = true; + CancelTimeout(); + + _ = RunAnimationAsync(destination, coordinatedElements); + return true; + } + + // Exposed internally so tests can verify disposal state without reflection. + internal bool IsDisposed => _disposed; + + /// + /// Releases all resources and cancels the animation if it is in flight. + /// The event is raised with Cancelled = true + /// only when the animation was actively running at dispose time. + /// + public void Dispose() + { + if (_disposed) return; + _disposed = true; + + CancelTimeout(); + _service.RemoveAnimation(_key); + _animationCts?.Cancel(); + _animationCts?.Dispose(); + _animationCts = null; + _animationTimer?.Stop(); + _animationTimer = null; + + var wasMidFlight = _activeDestination != null; + + if (_activeDestination != null) + { + _activeDestination.Opacity = _activeDestOriginalOpacity; + _activeDestination = null; + } + + if (_activeProxy != null && _activeOverlayLayer != null) + { + _activeOverlayLayer.Children.Remove(_activeProxy); + _activeProxy = null; + _activeOverlayLayer = null; + } + + _sourceSnapshot?.Dispose(); + _sourceSnapshot = null; + + if (wasMidFlight) + Completed?.Invoke(this, new ConnectedAnimationCompletedEventArgs(cancelled: true)); + } + + private void CaptureSnapshot(Visual source, TopLevel topLevel) + { + try + { + var dpi = topLevel.RenderScaling; + var w = (int)Math.Ceiling(source.Bounds.Width * dpi); + var h = (int)Math.Ceiling(source.Bounds.Height * dpi); + if (w > 0 && h > 0) + { + _sourceSnapshot = new RenderTargetBitmap( + new PixelSize(w, h), + new Vector(96 * dpi, 96 * dpi)); + _sourceSnapshot.Render(source); + } + } + catch (Exception ex) + { + Logger.TryGet(LogEventLevel.Warning, LogArea.Visual) + ?.Log(this, "ConnectedAnimation snapshot failed for key '{Key}': {Exception}", Key, ex); + _sourceSnapshot?.Dispose(); + _sourceSnapshot = null; + } + } + + private void CancelTimeout() + { + _timeoutTimerDisposable?.Dispose(); + _timeoutTimerDisposable = null; + _timeoutCts?.Cancel(); + _timeoutCts?.Dispose(); + _timeoutCts = null; + } + + private async Task RunAnimationAsync(Visual destination, IReadOnlyList coordinatedElements) + { + try + { + await RunAnimationCoreAsync(destination, coordinatedElements); + } + catch (OperationCanceledException) + { + // Dispose already handles cleanup. + } + catch (Exception ex) + { + Logger.TryGet(LogEventLevel.Warning, LogArea.Visual) + ?.Log(this, "ConnectedAnimation failed for key '{Key}': {Exception}", Key, ex); + Dispose(); + } + } + + private async Task RunAnimationCoreAsync(Visual destination, IReadOnlyList coordinatedElements) + { + ResolveTimingAndEasing(_service, out var duration, out var easing, + out var useGravityDip, out var useShadow); + + var topLevel = destination.FindAncestorOfType(); + if (topLevel == null) + { + OnAnimationComplete(); + return; + } + + var overlayLayer = OverlayLayer.GetOverlayLayer(topLevel); + if (overlayLayer == null) + { + await RunFallbackAnimationAsync(destination, coordinatedElements, + topLevel, duration, easing, useGravityDip, useShadow); + return; + } + + // Wait for destination layout if bounds are not yet valid. + if (destination.Bounds.Width <= 0 || destination.Bounds.Height <= 0 || + !destination.TransformToVisual(topLevel).HasValue) + { + if (destination is Layoutable layoutable) + { + var layoutTcs = new TaskCompletionSource( + TaskCreationOptions.RunContinuationsAsynchronously); + using var timeoutCts = new CancellationTokenSource(TimeSpan.FromMilliseconds(500)); + + EventHandler? handler = null; + handler = (_, _) => + { + if (destination.Bounds.Width > 0 && destination.Bounds.Height > 0 && + destination.TransformToVisual(topLevel).HasValue) + { + layoutable.LayoutUpdated -= handler; + layoutTcs.TrySetResult(true); + } + }; + layoutable.LayoutUpdated += handler; + + using var reg = timeoutCts.Token.Register(() => + { + layoutable.LayoutUpdated -= handler; + layoutTcs.TrySetResult(false); + }); + + await layoutTcs.Task; + } + } + + var destTransform = destination.TransformToVisual(topLevel); + if (!destTransform.HasValue) + { + OnAnimationComplete(); + return; + } + + var destBounds = new Rect( + destTransform.Value.Transform(new Point(0, 0)), + new Size(destination.Bounds.Width, destination.Bounds.Height)); + + var destCornerRadius = GetCornerRadius(destination); + var destBorderThickness = GetBorderThickness(destination); + var destBorderBrush = GetBorderBrush(destination); + + var proxy = new ConnectedAnimationProxy + { + Width = _sourceBounds.Width, + Height = _sourceBounds.Height, + CornerRadius = _sourceCornerRadius, + BorderThickness = _sourceBorderThickness, + BorderBrush = _sourceBorderBrush, + ClipToBounds = true, + IsHitTestVisible = false, + }; + + if (_sourceBackground != null) + proxy.Background = _sourceBackground; + else if (_sourceSnapshot != null) + proxy.Background = new ImageBrush(_sourceSnapshot) { Stretch = Stretch.Fill }; + + Canvas.SetLeft(proxy, _sourceBounds.X); + Canvas.SetTop(proxy, _sourceBounds.Y); + + var destOriginalOpacity = destination.Opacity; + destination.Opacity = 0; + + _activeDestination = destination; + _activeDestOriginalOpacity = destOriginalOpacity; + _activeProxy = proxy; + _activeOverlayLayer = overlayLayer; + + var originalOpacities = new double[coordinatedElements.Count]; + for (int i = 0; i < coordinatedElements.Count; i++) + { + if (ReferenceEquals(coordinatedElements[i], destination)) + continue; + originalOpacities[i] = coordinatedElements[i].Opacity; + coordinatedElements[i].Opacity = 0; + } + + var destBackground = GetBackground(destination); + var needsCrossFade = destBackground != null + && _sourceBackground != null + && !BrushesEqual(_sourceBackground, destBackground); + + overlayLayer.Children.Add(proxy); + + Border? crossFadeOverlay = null; + if (needsCrossFade) + { + crossFadeOverlay = new Border + { + Background = destBackground, + Opacity = 0, + IsHitTestVisible = false, + }; + proxy.Child = crossFadeOverlay; + } + + var startX = _sourceBounds.X; var endX = destBounds.X; + var startY = _sourceBounds.Y; var endY = destBounds.Y; + var startW = _sourceBounds.Width; var endW = destBounds.Width; + var startH = _sourceBounds.Height; var endH = destBounds.Height; + + var srcTL = _sourceCornerRadius.TopLeft; + var srcTR = _sourceCornerRadius.TopRight; + var srcBR = _sourceCornerRadius.BottomRight; + var srcBL = _sourceCornerRadius.BottomLeft; + var dstTL = destCornerRadius.TopLeft; + var dstTR = destCornerRadius.TopRight; + var dstBR = destCornerRadius.BottomRight; + var dstBL = destCornerRadius.BottomLeft; + + var srcBT = _sourceBorderThickness; + var dstBT = destBorderThickness; + + var canLerpBorderBrush = _sourceBorderBrush is ISolidColorBrush && destBorderBrush is ISolidColorBrush; + var srcBC = (_sourceBorderBrush as ISolidColorBrush)?.Color ?? default; + var dstBC = (destBorderBrush as ISolidColorBrush)?.Color ?? default; + SolidColorBrush? lerpBrush = canLerpBorderBrush ? new SolidColorBrush(srcBC) : null; + var snapBorderBrush = !canLerpBorderBrush && destBorderBrush != null; + + double dipAmplitude = 0, scaleAmplitude = 0; + if (useGravityDip) + { + var travel = Math.Max(Math.Abs(endX - startX), Math.Abs(endY - startY)); + dipAmplitude = Math.Clamp(travel * 0.12, 8, 50); + scaleAmplitude = 0.05; + } + + var animateShadow = useShadow && useGravityDip; + + _animationCts = new CancellationTokenSource(); + + proxy.ProgressCallback = progress => + { + var ep = easing.Ease(progress); + + var bx = startX + (endX - startX) * ep; + var by = startY + (endY - startY) * ep; + var bw = startW + (endW - startW) * ep; + var bh = startH + (endH - startH) * ep; + + if (useGravityDip) + { + var dipCurve = Math.Sin(Math.PI * progress); + var scaleBoost = 1.0 + scaleAmplitude * dipCurve; + var sw = bw * scaleBoost; + var sh = bh * scaleBoost; + + Canvas.SetLeft(proxy, bx - (sw - bw) / 2); + Canvas.SetTop(proxy, by - (sh - bh) / 2 + dipAmplitude * dipCurve); + proxy.Width = Math.Max(1, sw); + proxy.Height = Math.Max(1, sh); + + if (animateShadow) + { + var alpha = (byte)(100 * dipCurve); + var blur = 24 * dipCurve; + var offsetY = 10 * dipCurve; + proxy.BoxShadow = new BoxShadows(new BoxShadow + { + OffsetX = 0, OffsetY = offsetY, + Blur = blur, + Color = Color.FromArgb(alpha, 0, 0, 0) + }); + } + } + else + { + Canvas.SetLeft(proxy, bx); + Canvas.SetTop(proxy, by); + proxy.Width = Math.Max(1, bw); + proxy.Height = Math.Max(1, bh); + } + + proxy.CornerRadius = new CornerRadius( + srcTL + (dstTL - srcTL) * ep, + srcTR + (dstTR - srcTR) * ep, + srcBR + (dstBR - srcBR) * ep, + srcBL + (dstBL - srcBL) * ep); + + proxy.BorderThickness = new Thickness( + srcBT.Left + (dstBT.Left - srcBT.Left) * ep, + srcBT.Top + (dstBT.Top - srcBT.Top) * ep, + srcBT.Right + (dstBT.Right - srcBT.Right) * ep, + srcBT.Bottom + (dstBT.Bottom - srcBT.Bottom) * ep); + + if (lerpBrush != null) + { + lerpBrush.Color = Color.FromArgb( + (byte)(srcBC.A + (dstBC.A - srcBC.A) * ep), + (byte)(srcBC.R + (dstBC.R - srcBC.R) * ep), + (byte)(srcBC.G + (dstBC.G - srcBC.G) * ep), + (byte)(srcBC.B + (dstBC.B - srcBC.B) * ep)); + proxy.BorderBrush = lerpBrush; + } + else if (snapBorderBrush && progress >= 0.5) + { + proxy.BorderBrush = destBorderBrush; + snapBorderBrush = false; + } + + if (crossFadeOverlay != null) + crossFadeOverlay.Opacity = ep; + + if (progress > CoordinatedFadeStartThreshold) + { + var cp = (progress - CoordinatedFadeStartThreshold) / CoordinatedFadeRange; + for (int j = 0; j < coordinatedElements.Count; j++) + { + if (ReferenceEquals(coordinatedElements[j], destination)) + continue; + coordinatedElements[j].Opacity = originalOpacities[j] * cp; + } + } + }; + + var animation = new Avalonia.Animation.Animation + { + Duration = duration, + Easing = new LinearEasing(), + FillMode = FillMode.Forward, + Children = + { + new KeyFrame { Cue = new Cue(0), Setters = { new Setter(ConnectedAnimationProxy.ProgressProperty, 0.0) } }, + new KeyFrame { Cue = new Cue(1), Setters = { new Setter(ConnectedAnimationProxy.ProgressProperty, 1.0) } }, + } + }; + + await animation.RunAsync(proxy, _animationCts.Token); + + _animationCts?.Dispose(); + _animationCts = null; + + destination.Opacity = destOriginalOpacity; + + _activeDestination = null; + _activeProxy = null; + _activeOverlayLayer = null; + + overlayLayer.Children.Remove(proxy); + + for (int i = 0; i < coordinatedElements.Count; i++) + { + if (ReferenceEquals(coordinatedElements[i], destination)) + continue; + coordinatedElements[i].Opacity = originalOpacities[i]; + } + + _sourceSnapshot?.Dispose(); + _sourceSnapshot = null; + + OnAnimationComplete(); + } + + private async Task RunFallbackAnimationAsync( + Visual destination, IReadOnlyList coordinatedElements, + TopLevel topLevel, TimeSpan duration, Easing easing, + bool useGravityDip, bool useShadow) + { + var destTransform = destination.TransformToVisual(topLevel); + if (!destTransform.HasValue) { OnAnimationComplete(); return; } + + var destBounds = new Rect( + destTransform.Value.Transform(new Point(0, 0)), + new Size(destination.Bounds.Width, destination.Bounds.Height)); + + var dx = _sourceBounds.X - destBounds.X; + var dy = _sourceBounds.Y - destBounds.Y; + var sx = _sourceBounds.Width > 0 && destBounds.Width > 0 ? _sourceBounds.Width / destBounds.Width : 1.0; + var sy = _sourceBounds.Height > 0 && destBounds.Height > 0 ? _sourceBounds.Height / destBounds.Height : 1.0; + + var group = new TransformGroup(); + var scaleT = new ScaleTransform(sx, sy); + var transT = new TranslateTransform(dx, dy); + group.Children.Add(scaleT); + group.Children.Add(transT); + + var origTransform = destination.RenderTransform; + var origOrigin = destination.RenderTransformOrigin; + destination.RenderTransformOrigin = new RelativePoint(0, 0, RelativeUnit.Absolute); + destination.RenderTransform = group; + + double dipAmp = 0, scaleAmp = 0; + if (useGravityDip) + { + var travel = Math.Max(Math.Abs(dx), Math.Abs(dy)); + dipAmp = Math.Clamp(travel * 0.12, 8, 50); + scaleAmp = 0.05; + } + + var shadowBorder = useShadow && useGravityDip && destination is Border b ? b : null; + var origShadow = shadowBorder?.BoxShadow ?? default; + + var originalOpacities = new double[coordinatedElements.Count]; + for (int i = 0; i < coordinatedElements.Count; i++) + { + if (ReferenceEquals(coordinatedElements[i], destination)) + continue; + originalOpacities[i] = coordinatedElements[i].Opacity; + coordinatedElements[i].Opacity = 0; + } + + var startTimestamp = Stopwatch.GetTimestamp(); + var tcs = new TaskCompletionSource( + TaskCreationOptions.RunContinuationsAsynchronously); + + _animationTimer = new DispatcherTimer(DispatcherPriority.Render) + { + Interval = TimeSpan.FromMilliseconds(16) + }; + + _animationTimer.Tick += (_, _) => + { + var elapsed = Stopwatch.GetElapsedTime(startTimestamp).TotalMilliseconds; + var progress = Math.Min(1.0, elapsed / duration.TotalMilliseconds); + var ep = easing.Ease(progress); + + var bsx = sx + (1.0 - sx) * ep; + var bsy = sy + (1.0 - sy) * ep; + var btx = dx * (1.0 - ep); + var bty = dy * (1.0 - ep); + + if (useGravityDip) + { + var dipCurve = Math.Sin(Math.PI * progress); + var scaleBoost = 1.0 + scaleAmp * dipCurve; + scaleT.ScaleX = bsx * scaleBoost; + scaleT.ScaleY = bsy * scaleBoost; + transT.X = btx; + transT.Y = bty + dipAmp * dipCurve; + + if (shadowBorder != null) + { + var alpha = (byte)(100 * dipCurve); + var blur = 24 * dipCurve; + var offsetY = 10 * dipCurve; + shadowBorder.BoxShadow = new BoxShadows(new BoxShadow + { + OffsetX = 0, OffsetY = offsetY, + Blur = blur, + Color = Color.FromArgb(alpha, 0, 0, 0) + }); + } + } + else + { + scaleT.ScaleX = bsx; + scaleT.ScaleY = bsy; + transT.X = btx; + transT.Y = bty; + } + + if (progress > CoordinatedFadeStartThreshold) + { + var cp = (progress - CoordinatedFadeStartThreshold) / CoordinatedFadeRange; + for (int j = 0; j < coordinatedElements.Count; j++) + { + if (ReferenceEquals(coordinatedElements[j], destination)) + continue; + coordinatedElements[j].Opacity = originalOpacities[j] * cp; + } + } + + if (progress >= 1.0) + { + _animationTimer!.Stop(); + _animationTimer = null; + tcs.TrySetResult(true); + } + }; + + _animationCts = new CancellationTokenSource(); + using var reg = _animationCts.Token.Register(() => tcs.TrySetCanceled()); + + _animationTimer.Start(); + + bool cancelled = false; + try + { + await tcs.Task; + } + catch (OperationCanceledException) + { + cancelled = true; + } + finally + { + _animationTimer?.Stop(); + _animationTimer = null; + _animationCts?.Dispose(); + _animationCts = null; + } + + destination.RenderTransform = origTransform; + destination.RenderTransformOrigin = origOrigin; + + if (shadowBorder != null) + shadowBorder.BoxShadow = origShadow; + + for (int i = 0; i < coordinatedElements.Count; i++) + { + if (ReferenceEquals(coordinatedElements[i], destination)) + continue; + coordinatedElements[i].Opacity = originalOpacities[i]; + } + + _sourceSnapshot?.Dispose(); + _sourceSnapshot = null; + + if (cancelled) + { + Completed?.Invoke(this, new ConnectedAnimationCompletedEventArgs(cancelled: true)); + return; + } + + OnAnimationComplete(); + } + + internal void ResolveTimingAndEasing(ConnectedAnimationService service, + out TimeSpan duration, out Easing easing, + out bool useGravityDip, out bool useShadow) + { + if (Configuration is DirectConnectedAnimationConfiguration direct) + { + duration = direct.Duration ?? service.DefaultDuration; + easing = s_directEasing; + useGravityDip = false; + useShadow = false; + } + else if (Configuration is BasicConnectedAnimationConfiguration) + { + duration = service.DefaultDuration; + easing = service.DefaultEasingFunction ?? s_basicEasing; + useGravityDip = false; + useShadow = false; + } + else + { + duration = service.DefaultDuration; + easing = service.DefaultEasingFunction ?? s_gravityEasing; + useGravityDip = true; + useShadow = Configuration is GravityConnectedAnimationConfiguration g + ? g.IsShadowEnabled + : true; + } + } + + private void OnAnimationComplete() + { + _service.RemoveAnimation(_key); + Completed?.Invoke(this, new ConnectedAnimationCompletedEventArgs(cancelled: false)); + } + + private static IBrush? GetBackground(Visual visual) => visual switch + { + Border b => b.Background, + Panel p => p.Background, + ContentPresenter cp => cp.Background, + ContentControl cc => cc.Background, + TemplatedControl tc => tc.Background, + _ => null, + }; + + private static CornerRadius GetCornerRadius(Visual visual) => visual switch + { + Border b => b.CornerRadius, + TemplatedControl tc => tc.CornerRadius, + ContentPresenter cp => cp.CornerRadius, + _ => default, + }; + + private static Thickness GetBorderThickness(Visual visual) => visual switch + { + Border b => b.BorderThickness, + TemplatedControl tc => tc.BorderThickness, + ContentPresenter cp => cp.BorderThickness, + _ => default, + }; + + private static IBrush? GetBorderBrush(Visual visual) => visual switch + { + Border b => b.BorderBrush, + TemplatedControl tc => tc.BorderBrush, + ContentPresenter cp => cp.BorderBrush, + _ => null, + }; + + private static bool BrushesEqual(IBrush a, IBrush b) + { + if (ReferenceEquals(a, b)) return true; + if (a is ISolidColorBrush sa && b is ISolidColorBrush sb) + return sa.Color == sb.Color && Math.Abs(sa.Opacity - sb.Opacity) < 0.001; + return false; + } + + private class ConnectedAnimationProxy : Border + { + public static readonly StyledProperty ProgressProperty = + AvaloniaProperty.Register(nameof(Progress)); + + public double Progress + { + get => GetValue(ProgressProperty); + set => SetValue(ProgressProperty, value); + } + + internal Action? ProgressCallback { get; set; } + + protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change) + { + base.OnPropertyChanged(change); + if (change.Property == ProgressProperty) + ProgressCallback?.Invoke(change.GetNewValue()); + } + } + } +} diff --git a/src/Avalonia.Controls/Animation/ConnectedAnimationConfiguration.cs b/src/Avalonia.Controls/Animation/ConnectedAnimationConfiguration.cs new file mode 100644 index 0000000000..f4387aac44 --- /dev/null +++ b/src/Avalonia.Controls/Animation/ConnectedAnimationConfiguration.cs @@ -0,0 +1,56 @@ +using System; + +namespace Avalonia.Animation +{ + /// + /// Base class for connected animation configurations that control + /// the visual style and physics of the transition. + /// + internal abstract class ConnectedAnimationConfiguration + { + } + + /// + /// Produces a gravity-physics effect suitable for forward navigation: + /// the element arcs slightly as it travels and casts an animated shadow. + /// This is the default configuration when none is specified. + /// + /// + /// Use for back navigation + /// and for a plain transition. + /// + internal class GravityConnectedAnimationConfiguration : ConnectedAnimationConfiguration + { + /// + /// Gets or sets whether a drop shadow is rendered beneath the element + /// during the gravity arc. Defaults to . + /// + public bool IsShadowEnabled { get; set; } = true; + } + + /// + /// Produces a direct, linear translation suitable for back navigation. + /// No gravity arc or shadow is applied, and the default duration is shorter (150 ms). + /// + /// + /// Assign this to before calling + /// TryStart on the return animation to animate back to the source view. + /// + internal class DirectConnectedAnimationConfiguration : ConnectedAnimationConfiguration + { + /// + /// Gets or sets the duration of the animation. + /// When , is used. + /// + public TimeSpan? Duration { get; set; } + } + + /// + /// Produces a simple ease-in-out transition between the source and destination elements + /// with no gravity arc or shadow. Duration is taken from + /// . + /// + internal class BasicConnectedAnimationConfiguration : ConnectedAnimationConfiguration + { + } +} diff --git a/src/Avalonia.Controls/Animation/ConnectedAnimationService.cs b/src/Avalonia.Controls/Animation/ConnectedAnimationService.cs new file mode 100644 index 0000000000..e132e5f22c --- /dev/null +++ b/src/Avalonia.Controls/Animation/ConnectedAnimationService.cs @@ -0,0 +1,101 @@ +using System; +using System.Collections.Generic; +using System.Runtime.CompilerServices; +using Avalonia.Animation.Easings; +using Avalonia.Controls; + +namespace Avalonia.Animation +{ + /// + /// Coordinates connected animations across views. + /// Each window has its own independent instance so + /// animations cannot bleed across windows. + /// + /// + /// Typical usage: + /// + /// On the source view, call to capture the element. + /// Navigate to the destination view. + /// On the destination view, call then + /// TryStart on the returned animation to run the animation. + /// + /// + internal class ConnectedAnimationService : AvaloniaObject + { + private static readonly ConditionalWeakTable s_perView = new(); + private readonly Dictionary _animations = new(); + + internal ConnectedAnimationService() + { + DefaultDuration = TimeSpan.FromMilliseconds(300); + } + + /// + /// Gets the for the specified . + /// Each top-level window has its own isolated instance. + /// + public static ConnectedAnimationService GetForTopLevel(TopLevel topLevel) + { + ArgumentNullException.ThrowIfNull(topLevel); + return s_perView.GetValue(topLevel, static _ => new ConnectedAnimationService()); + } + + /// + /// Gets or sets the default duration applied to all animations whose + /// configuration does not specify one. Defaults to 300 ms. + /// + public TimeSpan DefaultDuration { get; set; } + + /// + /// Gets or sets the default easing function applied when the active + /// does not specify one. + /// When a configuration-specific default is used. + /// + public Easing? DefaultEasingFunction { get; set; } + + /// + /// Captures and registers a pending animation under + /// . Call this on the source view before navigating away. + /// + /// Unique string that pairs this call with the matching + /// call on the destination view. + /// The element to animate from. + /// The prepared . + public ConnectedAnimation PrepareToAnimate(string key, Visual source) + { + ArgumentException.ThrowIfNullOrEmpty(key); + ArgumentNullException.ThrowIfNull(source); + + // Replace any stale animation registered under the same key. + if (_animations.TryGetValue(key, out var old)) + { + _animations.Remove(key); + old.Dispose(); + } + + var animation = new ConnectedAnimation(key, source, this); + _animations[key] = animation; + return animation; + } + + /// + /// Retrieves a pending animation registered under . + /// Returns if no animation exists or if it has already been consumed. + /// Call this on the destination view after navigating, then call + /// TryStart on the returned animation. + /// + public ConnectedAnimation? GetAnimation(string key) + { + if (_animations.TryGetValue(key, out var animation) && !animation.IsConsumed) + return animation; + + return null; + } + + /// + /// Removes the animation registered under . + /// The caller is responsible for disposing the animation separately. + /// + internal void RemoveAnimation(string key) => _animations.Remove(key); + } +} diff --git a/src/Avalonia.Controls/AppBuilder.cs b/src/Avalonia.Controls/AppBuilder.cs index 9259be8594..bc299bdbe1 100644 --- a/src/Avalonia.Controls/AppBuilder.cs +++ b/src/Avalonia.Controls/AppBuilder.cs @@ -60,6 +60,16 @@ namespace Avalonia /// public string? RenderingSubsystemName { get; private set; } + /// + /// Gets or sets a method to call the initialize the text shaping subsystem. + /// + public Action? TextShapingSubsystemInitializer { get; private set; } + + /// + /// Gets the name of the currently selected text shaping subsystem. + /// + public string? TextShapingSubsystemName { get; private set; } + /// /// Gets a method to call after the is setup. /// @@ -224,6 +234,19 @@ namespace Avalonia RenderingSubsystemName = name; return Self; } + + /// + /// Specifies a text shaping subsystem to use. + /// + /// The method to call to initialize the text shaping subsystem. + /// The name of the text shaping subsystem. + /// An instance. + public AppBuilder UseTextShapingSubsystem(Action initializer, string name = "") + { + TextShapingSubsystemInitializer = initializer; + TextShapingSubsystemName = name; + return Self; + } /// /// Specifies a runtime platform subsystem to use. @@ -308,7 +331,12 @@ namespace Avalonia if (RenderingSubsystemInitializer == null) { - throw new InvalidOperationException("No rendering system configured."); + throw new InvalidOperationException("No rendering system configured. Consider calling UseSkia()."); + } + + if (TextShapingSubsystemInitializer == null) + { + throw new InvalidOperationException("No text shaping system configured. Consider calling UseHarfBuzz()."); } if (_appFactory == null) @@ -333,6 +361,7 @@ namespace Avalonia { _optionsInitializers?.Invoke(); RuntimePlatformServicesInitializer?.Invoke(); + TextShapingSubsystemInitializer?.Invoke(); RenderingSubsystemInitializer?.Invoke(); WindowingSubsystemInitializer?.Invoke(); AfterPlatformServicesSetupCallback?.Invoke(Self); diff --git a/src/Avalonia.Controls/AutoCompleteBox/AutoCompleteBox.cs b/src/Avalonia.Controls/AutoCompleteBox/AutoCompleteBox.cs index 880fd0ac8a..0520d7643f 100644 --- a/src/Avalonia.Controls/AutoCompleteBox/AutoCompleteBox.cs +++ b/src/Avalonia.Controls/AutoCompleteBox/AutoCompleteBox.cs @@ -740,7 +740,7 @@ namespace Avalonia.Controls /// /// A /// that contains the event data. - protected override void OnGotFocus(GotFocusEventArgs e) + protected override void OnGotFocus(FocusChangedEventArgs e) { base.OnGotFocus(e); FocusChanged(HasFocus()); @@ -752,7 +752,7 @@ namespace Avalonia.Controls /// /// A /// that contains the event data. - protected override void OnLostFocus(RoutedEventArgs e) + protected override void OnLostFocus(FocusChangedEventArgs e) { base.OnLostFocus(e); FocusChanged(HasFocus()); diff --git a/src/Avalonia.Controls/Automation/AutomationProperties.cs b/src/Avalonia.Controls/Automation/AutomationProperties.cs index e46dcb0eb2..b8d22cb30a 100644 --- a/src/Avalonia.Controls/Automation/AutomationProperties.cs +++ b/src/Avalonia.Controls/Automation/AutomationProperties.cs @@ -93,6 +93,29 @@ namespace Avalonia.Automation "ControlTypeOverride", typeof(AutomationProperties)); + /// + /// Defines the AutomationProperties.ClassNameOverride attached property. + /// + /// + /// This property affects the default value for . + /// + public static readonly AttachedProperty ClassNameOverrideProperty = + AvaloniaProperty.RegisterAttached( + "ClassNameOverride", + typeof(AutomationProperties)); + + /// + /// Defines the AutomationProperties.IsControlElementOverride attached property. + /// + /// + /// This property affects the default value for + /// . + /// + public static readonly AttachedProperty IsControlElementOverrideProperty = + AvaloniaProperty.RegisterAttached( + "IsControlElementOverride", + typeof(AutomationProperties)); + /// /// Defines the AutomationProperties.HelpText attached property. /// @@ -352,6 +375,42 @@ namespace Avalonia.Automation return element.GetValue(ControlTypeOverrideProperty); } + /// + /// Helper for setting the value of the on a StyledElement. + /// + public static void SetClassNameOverride(StyledElement element, string? value) + { + _ = element ?? throw new ArgumentNullException(nameof(element)); + element.SetValue(ClassNameOverrideProperty, value); + } + + /// + /// Helper for reading the value of the on a StyledElement. + /// + public static string? GetClassNameOverride(StyledElement element) + { + _ = element ?? throw new ArgumentNullException(nameof(element)); + return element.GetValue(ClassNameOverrideProperty); + } + + /// + /// Helper for setting the value of the on a StyledElement. + /// + public static void SetIsControlElementOverride(StyledElement element, bool? value) + { + _ = element ?? throw new ArgumentNullException(nameof(element)); + element.SetValue(IsControlElementOverrideProperty, value); + } + + /// + /// Helper for reading the value of the on a StyledElement. + /// + public static bool? GetIsControlElementOverride(StyledElement element) + { + _ = element ?? throw new ArgumentNullException(nameof(element)); + return element.GetValue(IsControlElementOverrideProperty); + } + /// /// Helper for setting the value of the on a StyledElement. /// diff --git a/src/Avalonia.Controls/Automation/Peers/AutomationPeer.cs b/src/Avalonia.Controls/Automation/Peers/AutomationPeer.cs index 47754c9b50..b6b056658b 100644 --- a/src/Avalonia.Controls/Automation/Peers/AutomationPeer.cs +++ b/src/Avalonia.Controls/Automation/Peers/AutomationPeer.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using Avalonia.Automation.Provider; +using Avalonia.Metadata; namespace Avalonia.Automation.Peers { @@ -205,7 +206,7 @@ namespace Avalonia.Automation.Peers /// /// /// - public string GetClassName() => GetClassNameCore() ?? string.Empty; + public string GetClassName() => GetClassNameOverrideCore() ?? string.Empty; /// /// Gets the automation peer for the label that is targeted to the element. @@ -398,13 +399,25 @@ namespace Avalonia.Automation.Peers /// Windows /// No mapping, but used internally to translate coordinates. /// + /// + /// + [PrivateApi] + public AutomationPeer? GetVisualRoot() => GetVisualRootCore(); + + /// + /// Gets the that is the root of this 's + /// visual tree. + /// + /// + /// /// /// macOS /// NSAccessibilityProtocol.accessibilityTopLevelUIElement /// /// /// - public AutomationPeer? GetVisualRoot() => GetVisualRootCore(); + [PrivateApi] + public AutomationPeer? GetAutomationRoot() => GetAutomationRootCore(); /// /// Gets a value that indicates whether the element that is associated with this automation @@ -633,7 +646,12 @@ namespace Avalonia.Automation.Peers return GetAutomationControlTypeCore(); } - protected virtual AutomationPeer? GetVisualRootCore() + protected virtual string GetClassNameOverrideCore() + { + return GetClassNameCore(); + } + + private protected virtual AutomationPeer? GetAutomationRootCore() { var peer = this; var parent = peer.GetParent(); @@ -647,6 +665,8 @@ namespace Avalonia.Automation.Peers return peer; } + private protected virtual AutomationPeer? GetVisualRootCore() => GetAutomationRootCore(); + protected virtual bool IsContentElementOverrideCore() { diff --git a/src/Avalonia.Controls/Automation/Peers/CarouselPageAutomationPeer.cs b/src/Avalonia.Controls/Automation/Peers/CarouselPageAutomationPeer.cs new file mode 100644 index 0000000000..0744e43125 --- /dev/null +++ b/src/Avalonia.Controls/Automation/Peers/CarouselPageAutomationPeer.cs @@ -0,0 +1,26 @@ +using Avalonia.Controls; + +namespace Avalonia.Automation.Peers; + +public class CarouselPageAutomationPeer : ControlAutomationPeer +{ + public CarouselPageAutomationPeer(CarouselPage owner) + : base(owner) + { + } + + public new CarouselPage Owner => (CarouselPage)base.Owner; + + protected override AutomationControlType GetAutomationControlTypeCore() + => AutomationControlType.Pane; + + protected override string? GetNameCore() + { + var result = base.GetNameCore(); + + if (string.IsNullOrEmpty(result)) + result = Owner.Header?.ToString(); + + return result; + } +} diff --git a/src/Avalonia.Controls/Automation/Peers/ContentPageAutomationPeer.cs b/src/Avalonia.Controls/Automation/Peers/ContentPageAutomationPeer.cs new file mode 100644 index 0000000000..1c546aa7ca --- /dev/null +++ b/src/Avalonia.Controls/Automation/Peers/ContentPageAutomationPeer.cs @@ -0,0 +1,26 @@ +using Avalonia.Controls; + +namespace Avalonia.Automation.Peers; + +public class ContentPageAutomationPeer : ControlAutomationPeer +{ + public ContentPageAutomationPeer(ContentPage owner) + : base(owner) + { + } + + public new ContentPage Owner => (ContentPage)base.Owner; + + protected override AutomationControlType GetAutomationControlTypeCore() + => AutomationControlType.Pane; + + protected override string? GetNameCore() + { + var result = base.GetNameCore(); + + if (string.IsNullOrEmpty(result)) + result = Owner.Header?.ToString(); + + return result; + } +} diff --git a/src/Avalonia.Controls/Automation/Peers/ControlAutomationPeer.cs b/src/Avalonia.Controls/Automation/Peers/ControlAutomationPeer.cs index aaabc6f495..59bfc6a045 100644 --- a/src/Avalonia.Controls/Automation/Peers/ControlAutomationPeer.cs +++ b/src/Avalonia.Controls/Automation/Peers/ControlAutomationPeer.cs @@ -147,10 +147,10 @@ namespace Avalonia.Automation.Peers EnsureConnected(); return _parent; } - - protected override AutomationPeer? GetVisualRootCore() + + private protected override AutomationPeer? GetVisualRootCore() { - if (Owner.GetVisualRoot() is Control c) + if (Owner?.PresentationSource?.InputRoot?.FocusRoot is Control c) return CreatePeerForElement(c); return null; } @@ -219,6 +219,11 @@ namespace Avalonia.Automation.Peers return AutomationProperties.GetControlTypeOverride(Owner) ?? GetAutomationControlTypeCore(); } + protected override string GetClassNameOverrideCore() + { + return AutomationProperties.GetClassNameOverride(Owner) ?? GetClassNameCore(); + } + protected override bool IsContentElementOverrideCore() { var view = AutomationProperties.GetAccessibilityView(Owner); @@ -227,6 +232,8 @@ namespace Avalonia.Automation.Peers protected override bool IsControlElementOverrideCore() { + if (AutomationProperties.GetIsControlElementOverride(Owner) is { } isControlElement) + return isControlElement; var view = AutomationProperties.GetAccessibilityView(Owner); return view == AccessibilityView.Default ? IsControlElementCore() : view >= AccessibilityView.Control; } @@ -268,11 +275,13 @@ namespace Avalonia.Automation.Peers private void VisualChildrenChanged(object? sender, EventArgs e) => InvalidateChildren(); + private protected virtual Visual? GetVisualParent() => Owner.GetVisualParent(); + private void OwnerPropertyChanged(object? sender, AvaloniaPropertyChangedEventArgs e) { if (e.Property == Visual.IsVisibleProperty) { - var parent = Owner.GetVisualParent(); + var parent = GetVisualParent(); if (parent is Control c) (GetOrCreate(c) as ControlAutomationPeer)?.InvalidateChildren(); } @@ -303,7 +312,7 @@ namespace Avalonia.Automation.Peers { if (!_parentValid) { - var parent = Owner.GetVisualParent(); + var parent = GetVisualParent(); while (parent is object) { @@ -311,6 +320,11 @@ namespace Avalonia.Automation.Peers { var parentPeer = GetOrCreate(c); parentPeer.GetChildren(); + if (parentPeer is ControlAutomationPeer controlPeer) + { + parent = controlPeer.GetVisualParent(); + continue; + } } parent = parent.GetVisualParent(); diff --git a/src/Avalonia.Controls/Automation/Peers/DrawerPageAutomationPeer.cs b/src/Avalonia.Controls/Automation/Peers/DrawerPageAutomationPeer.cs new file mode 100644 index 0000000000..39a009967e --- /dev/null +++ b/src/Avalonia.Controls/Automation/Peers/DrawerPageAutomationPeer.cs @@ -0,0 +1,50 @@ +using Avalonia.Automation.Provider; +using Avalonia.Controls; + +namespace Avalonia.Automation.Peers; + +public class DrawerPageAutomationPeer : ControlAutomationPeer, + IExpandCollapseProvider +{ + public DrawerPageAutomationPeer(DrawerPage owner) + : base(owner) + { + owner.PropertyChanged += OwnerPropertyChanged; + } + + public new DrawerPage Owner => (DrawerPage)base.Owner; + + public ExpandCollapseState ExpandCollapseState => ToState(Owner.IsOpen); + public bool ShowsMenu => false; + public void Collapse() => Owner.IsOpen = false; + public void Expand() => Owner.IsOpen = true; + + protected override AutomationControlType GetAutomationControlTypeCore() + => AutomationControlType.Pane; + + protected override string? GetNameCore() + { + var result = base.GetNameCore(); + + if (string.IsNullOrEmpty(result)) + result = Owner.Header?.ToString(); + + return result; + } + + private void OwnerPropertyChanged(object? sender, AvaloniaPropertyChangedEventArgs e) + { + if (e.Property == DrawerPage.IsOpenProperty) + { + RaisePropertyChangedEvent( + ExpandCollapsePatternIdentifiers.ExpandCollapseStateProperty, + ToState(e.GetOldValue()), + ToState(e.GetNewValue())); + } + } + + private static ExpandCollapseState ToState(bool value) + { + return value ? ExpandCollapseState.Expanded : ExpandCollapseState.Collapsed; + } +} diff --git a/src/Avalonia.Controls/Automation/Peers/EmbeddableControlRootAutomationPeer.cs b/src/Avalonia.Controls/Automation/Peers/EmbeddableControlRootAutomationPeer.cs index cbc45d113e..1aeeeec3d1 100644 --- a/src/Avalonia.Controls/Automation/Peers/EmbeddableControlRootAutomationPeer.cs +++ b/src/Avalonia.Controls/Automation/Peers/EmbeddableControlRootAutomationPeer.cs @@ -58,8 +58,8 @@ namespace Avalonia.Controls.Automation.Peers { var oldFocus = _focus; var c = focus as Control; - - _focus = c?.VisualRoot == Owner ? c : null; + + _focus = Owner.IsVisualAncestorOf(c) ? c : null; if (_focus != oldFocus) { @@ -89,5 +89,7 @@ namespace Avalonia.Controls.Automation.Peers Owner.Closed -= OnClosed; StopTrackingFocus(); } + + private protected override Visual? GetVisualParent() => null; } } diff --git a/src/Avalonia.Controls/Automation/Peers/NavigationPageAutomationPeer.cs b/src/Avalonia.Controls/Automation/Peers/NavigationPageAutomationPeer.cs new file mode 100644 index 0000000000..ea7e03f7b4 --- /dev/null +++ b/src/Avalonia.Controls/Automation/Peers/NavigationPageAutomationPeer.cs @@ -0,0 +1,29 @@ +using Avalonia.Controls; + +namespace Avalonia.Automation.Peers; + +public class NavigationPageAutomationPeer : ControlAutomationPeer +{ + public NavigationPageAutomationPeer(NavigationPage owner) + : base(owner) + { + } + + public new NavigationPage Owner => (NavigationPage)base.Owner; + + protected override AutomationControlType GetAutomationControlTypeCore() + => AutomationControlType.Pane; + + protected override string? GetNameCore() + { + var result = base.GetNameCore(); + + if (string.IsNullOrEmpty(result)) + result = Owner.Header?.ToString(); + + if (string.IsNullOrEmpty(result)) + result = Owner.CurrentPage?.Header?.ToString(); + + return result; + } +} diff --git a/src/Avalonia.Controls/Automation/Peers/PipsPagerAutomationPeer.cs b/src/Avalonia.Controls/Automation/Peers/PipsPagerAutomationPeer.cs new file mode 100644 index 0000000000..b40a9b4159 --- /dev/null +++ b/src/Avalonia.Controls/Automation/Peers/PipsPagerAutomationPeer.cs @@ -0,0 +1,85 @@ +using System.Collections.Generic; +using Avalonia.Automation.Provider; +using Avalonia.Controls; + +namespace Avalonia.Automation.Peers +{ + /// + /// An automation peer for . + /// + public class PipsPagerAutomationPeer : ControlAutomationPeer, ISelectionProvider + { + private ListBox? _pipsList; + + /// + /// Initializes a new instance of the class. + /// + /// The control associated with this peer. + public PipsPagerAutomationPeer(PipsPager owner) : base(owner) + { + owner.SelectedIndexChanged += OnSelectionChanged; + } + + /// + /// Gets the owner as a . + /// + private new PipsPager Owner => (PipsPager)base.Owner; + + /// + public bool CanSelectMultiple => false; + + /// + public bool IsSelectionRequired => true; + + /// + public IReadOnlyList GetSelection() + { + var result = new List(); + var owner = Owner; + + if (owner.SelectedPageIndex >= 0 && owner.SelectedPageIndex < owner.NumberOfPages) + { + _pipsList ??= owner.FindNameScope()?.Find("PART_PipsPagerList"); + + if (_pipsList != null) + { + var container = _pipsList.ContainerFromIndex(owner.SelectedPageIndex); + if (container is Control c) + { + var peer = GetOrCreate(c); + result.Add(peer); + } + } + } + + return result; + } + + /// + protected override AutomationControlType GetAutomationControlTypeCore() + { + return AutomationControlType.List; + } + + /// + protected override string GetClassNameCore() + { + return nameof(PipsPager); + } + + /// + protected override string? GetNameCore() + { + var name = base.GetNameCore(); + return string.IsNullOrWhiteSpace(name) ? "Pips Pager" : name; + } + + private void OnSelectionChanged(object? sender, Controls.PipsPagerSelectedIndexChangedEventArgs e) + { + RaisePropertyChangedEvent( + SelectionPatternIdentifiers.SelectionProperty, + e.OldIndex, + e.NewIndex); + } + } +} diff --git a/src/Avalonia.Controls/Automation/Peers/TabbedPageAutomationPeer.cs b/src/Avalonia.Controls/Automation/Peers/TabbedPageAutomationPeer.cs new file mode 100644 index 0000000000..e679d7109b --- /dev/null +++ b/src/Avalonia.Controls/Automation/Peers/TabbedPageAutomationPeer.cs @@ -0,0 +1,42 @@ +using Avalonia.Controls; + +namespace Avalonia.Automation.Peers; + +public class TabbedPageAutomationPeer : ControlAutomationPeer +{ + public TabbedPageAutomationPeer(TabbedPage owner) + : base(owner) + { + } + + public new TabbedPage Owner => (TabbedPage)base.Owner; + + protected override AutomationControlType GetAutomationControlTypeCore() + => AutomationControlType.Pane; + + protected override string? GetNameCore() + { + var result = base.GetNameCore(); + + if (string.IsNullOrEmpty(result)) + result = Owner.Header?.ToString(); + + var index = Owner.SelectedIndex; + var tabCount = GetTabCount(); + + if (index >= 0 && tabCount > 0) + { + var header = Owner.SelectedPage?.Header?.ToString(); + var position = $"Tab {index + 1} of {tabCount}"; + var tabName = string.IsNullOrEmpty(header) ? position : $"{position}: {header}"; + return string.IsNullOrEmpty(result) ? tabName : $"{result} {tabName}"; + } + + return result; + } + + private int GetTabCount() + { + return Owner.GetTabCount(); + } +} diff --git a/src/Avalonia.Controls/Automation/Peers/TitleBarAutomationPeer.cs b/src/Avalonia.Controls/Automation/Peers/TitleBarAutomationPeer.cs deleted file mode 100644 index 4bd606dd6e..0000000000 --- a/src/Avalonia.Controls/Automation/Peers/TitleBarAutomationPeer.cs +++ /dev/null @@ -1,26 +0,0 @@ -using Avalonia.Automation; -using Avalonia.Automation.Peers; -using Avalonia.Controls.Chrome; - -namespace Avalonia.Controls.Automation.Peers; - -internal class TitleBarAutomationPeer : ControlAutomationPeer -{ - public TitleBarAutomationPeer(TitleBar owner) : base(owner) - { - } - - protected override bool IsContentElementCore() => false; - - protected override string GetClassNameCore() - { - return "TitleBar"; - } - - protected override string? GetAutomationIdCore() => base.GetAutomationIdCore() ?? "AvaloniaTitleBar"; - - protected override AutomationControlType GetAutomationControlTypeCore() - { - return AutomationControlType.TitleBar; - } -} diff --git a/src/Avalonia.Controls/Automation/Peers/WindowAutomationPeer.cs b/src/Avalonia.Controls/Automation/Peers/WindowAutomationPeer.cs index 1162132d54..983b92313e 100644 --- a/src/Avalonia.Controls/Automation/Peers/WindowAutomationPeer.cs +++ b/src/Avalonia.Controls/Automation/Peers/WindowAutomationPeer.cs @@ -1,4 +1,6 @@ using System; +using System.Collections.Generic; +using Avalonia.Automation.Peers; using Avalonia.Controls; namespace Avalonia.Automation.Peers @@ -19,12 +21,23 @@ namespace Avalonia.Automation.Peers protected override string? GetNameCore() => Owner.Title; + protected override IReadOnlyList? GetChildrenCore() + { + var baseChildren = base.GetChildrenCore(); + var overlayPeer = Owner.TopLevelHost.GetOrCreateDecorationsOverlaysPeer(); + + var rv = new List { overlayPeer }; + if (baseChildren?.Count > 0) + rv.AddRange(baseChildren); + return rv; + } + private void OnOpened(object? sender, EventArgs e) { Owner.Opened -= OnOpened; StartTrackingFocus(); } - + private void OnClosed(object? sender, EventArgs e) { Owner.Closed -= OnClosed; diff --git a/src/Avalonia.Controls/Automation/Peers/WindowBaseAutomationPeer.cs b/src/Avalonia.Controls/Automation/Peers/WindowBaseAutomationPeer.cs index ceb695422d..ac645c80dd 100644 --- a/src/Avalonia.Controls/Automation/Peers/WindowBaseAutomationPeer.cs +++ b/src/Avalonia.Controls/Automation/Peers/WindowBaseAutomationPeer.cs @@ -70,7 +70,7 @@ namespace Avalonia.Automation.Peers var oldFocus = _focus; var c = focus as Control; - _focus = c?.VisualRoot == Owner ? c : null; + _focus = Owner.IsVisualAncestorOf(c) ? c : null; if (_focus != oldFocus) { @@ -88,6 +88,11 @@ namespace Avalonia.Automation.Peers OnFocusChanged(KeyboardDevice.Instance!.FocusedElement); } } + + private protected override Visual? GetVisualParent() + { + return null; + } } } diff --git a/src/Avalonia.Controls/Automation/Provider/IEmbeddedRootProvider.cs b/src/Avalonia.Controls/Automation/Provider/IEmbeddedRootProvider.cs index 4bbb2669d7..e745665bcf 100644 --- a/src/Avalonia.Controls/Automation/Provider/IEmbeddedRootProvider.cs +++ b/src/Avalonia.Controls/Automation/Provider/IEmbeddedRootProvider.cs @@ -1,5 +1,6 @@ using System; using Avalonia.Automation.Peers; +using Avalonia.Metadata; namespace Avalonia.Automation.Provider { @@ -12,6 +13,7 @@ namespace Avalonia.Automation.Provider /// an automation tree from a 3rd party UI framework that wishes to use Avalonia's automation /// support. /// + [PrivateApi] public interface IEmbeddedRootProvider { /// diff --git a/src/Avalonia.Controls/Automation/Provider/IRootProvider.cs b/src/Avalonia.Controls/Automation/Provider/IRootProvider.cs index 8452574df2..77ad98cbd7 100644 --- a/src/Avalonia.Controls/Automation/Provider/IRootProvider.cs +++ b/src/Avalonia.Controls/Automation/Provider/IRootProvider.cs @@ -1,5 +1,6 @@ using System; using Avalonia.Automation.Peers; +using Avalonia.Metadata; using Avalonia.Platform; namespace Avalonia.Automation.Provider @@ -13,6 +14,7 @@ namespace Avalonia.Automation.Provider /// be implemented on true root elements, such as Windows. To embed an automation tree, use /// instead. /// + [PrivateApi] public interface IRootProvider { /// diff --git a/src/Avalonia.Controls/Border.cs b/src/Avalonia.Controls/Border.cs index b816858632..29a31d8070 100644 --- a/src/Avalonia.Controls/Border.cs +++ b/src/Avalonia.Controls/Border.cs @@ -38,7 +38,7 @@ namespace Avalonia.Controls /// Defines the property. /// public static readonly StyledProperty BorderThicknessProperty = - AvaloniaProperty.Register(nameof(BorderThickness)); + AvaloniaProperty.Register(nameof(BorderThickness), validate: MarginProperty.ValidateValue); /// /// Defines the property. diff --git a/src/Avalonia.Controls/Button.cs b/src/Avalonia.Controls/Button.cs index 3652acac45..c094db57ec 100644 --- a/src/Avalonia.Controls/Button.cs +++ b/src/Avalonia.Controls/Button.cs @@ -208,14 +208,14 @@ namespace Avalonia.Controls if (IsDefault) { - if (e.Root is IInputElement inputElement) + if (e.RootVisual is IInputElement inputElement) { ListenForDefault(inputElement); } } if (IsCancel) { - if (e.Root is IInputElement inputElement) + if (e.RootVisual is IInputElement inputElement) { ListenForCancel(inputElement); } @@ -229,14 +229,14 @@ namespace Avalonia.Controls if (IsDefault) { - if (e.Root is IInputElement inputElement) + if (e.RootVisual is IInputElement inputElement) { StopListeningForDefault(inputElement); } } if (IsCancel) { - if (e.Root is IInputElement inputElement) + if (e.RootVisual is IInputElement inputElement) { StopListeningForCancel(inputElement); } @@ -453,7 +453,7 @@ namespace Avalonia.Controls } /// - protected override void OnLostFocus(RoutedEventArgs e) + protected override void OnLostFocus(FocusChangedEventArgs e) { base.OnLostFocus(e); @@ -543,10 +543,13 @@ namespace Avalonia.Controls oldFlyout.Hide(); } + (oldFlyout as PopupFlyoutBase)?.SetDefaultPlacementTarget(null); + // Must unregister events here while a reference to the old flyout still exists UnregisterFlyoutEvents(oldFlyout); RegisterFlyoutEvents(newFlyout); + (newFlyout as PopupFlyoutBase)?.SetDefaultPlacementTarget(this); UpdatePseudoClasses(); } } diff --git a/src/Avalonia.Controls/Calendar/Calendar.cs b/src/Avalonia.Controls/Calendar/Calendar.cs index d0a5e2e890..dc8ea002d9 100644 --- a/src/Avalonia.Controls/Calendar/Calendar.cs +++ b/src/Avalonia.Controls/Calendar/Calendar.cs @@ -2118,7 +2118,7 @@ namespace Avalonia.Controls } } - protected override void OnGotFocus(GotFocusEventArgs e) + protected override void OnGotFocus(FocusChangedEventArgs e) { base.OnGotFocus(e); HasFocusInternal = true; @@ -2157,7 +2157,7 @@ namespace Avalonia.Controls } } - protected override void OnLostFocus(RoutedEventArgs e) + protected override void OnLostFocus(FocusChangedEventArgs e) { base.OnLostFocus(e); HasFocusInternal = false; diff --git a/src/Avalonia.Controls/Calendar/CalendarItem.cs b/src/Avalonia.Controls/Calendar/CalendarItem.cs index 2bfe3c9be2..3d548e15b9 100644 --- a/src/Avalonia.Controls/Calendar/CalendarItem.cs +++ b/src/Avalonia.Controls/Calendar/CalendarItem.cs @@ -299,6 +299,19 @@ namespace Avalonia.Controls.Primitives } } + /// + protected override void OnDetachedFromVisualTree(VisualTreeAttachmentEventArgs e) + { + base.OnDetachedFromVisualTree(e); + + // Reset mouse button tracking state. When the calendar popup closes + // (e.g. due to a programmatic window change during date selection), + // the PointerReleased event never fires, leaving these flags stuck. + // See https://github.com/AvaloniaUI/Avalonia/issues/18418 + _isMouseLeftButtonDown = false; + _isMouseLeftButtonDownYearView = false; + } + private void SetDayTitles() { for (int childIndex = 0; childIndex < Calendar.ColumnsPerMonth; childIndex++) diff --git a/src/Avalonia.Controls/CalendarDatePicker/CalendarDatePicker.cs b/src/Avalonia.Controls/CalendarDatePicker/CalendarDatePicker.cs index c07d43b77c..6a3f44aa5f 100644 --- a/src/Avalonia.Controls/CalendarDatePicker/CalendarDatePicker.cs +++ b/src/Avalonia.Controls/CalendarDatePicker/CalendarDatePicker.cs @@ -390,7 +390,7 @@ namespace Avalonia.Controls } /// - protected override void OnGotFocus(GotFocusEventArgs e) + protected override void OnGotFocus(FocusChangedEventArgs e) { base.OnGotFocus(e); if(IsEnabled && _textBox != null && e.NavigationMethod == NavigationMethod.Tab) @@ -406,7 +406,7 @@ namespace Avalonia.Controls } /// - protected override void OnLostFocus(RoutedEventArgs e) + protected override void OnLostFocus(FocusChangedEventArgs e) { base.OnLostFocus(e); diff --git a/src/Avalonia.Controls/Carousel.cs b/src/Avalonia.Controls/Carousel.cs index 533f7bb626..6e8ce8a287 100644 --- a/src/Avalonia.Controls/Carousel.cs +++ b/src/Avalonia.Controls/Carousel.cs @@ -1,11 +1,13 @@ using Avalonia.Animation; using Avalonia.Controls.Primitives; using Avalonia.Controls.Templates; +using Avalonia.Input; namespace Avalonia.Controls { /// - /// An items control that displays its items as pages that fill the control. + /// An items control that displays its items as pages and can reveal adjacent pages + /// using . /// public class Carousel : SelectingItemsControl { @@ -16,13 +18,36 @@ namespace Avalonia.Controls AvaloniaProperty.Register(nameof(PageTransition)); /// - /// The default value of for + /// Defines the property. + /// + public static readonly StyledProperty IsSwipeEnabledProperty = + AvaloniaProperty.Register(nameof(IsSwipeEnabled), defaultValue: false); + + /// + /// Defines the property. + /// + public static readonly StyledProperty ViewportFractionProperty = + AvaloniaProperty.Register( + nameof(ViewportFraction), + defaultValue: 1d, + coerce: (_, value) => double.IsFinite(value) && value > 0 ? value : 1d); + + /// + /// Defines the property. + /// + public static readonly DirectProperty IsSwipingProperty = + AvaloniaProperty.RegisterDirect(nameof(IsSwiping), + o => o.IsSwiping); + + /// + /// The default value of for /// . /// private static readonly FuncTemplate DefaultPanel = new(() => new VirtualizingCarouselPanel()); private IScrollable? _scroller; + private bool _isSwiping; /// /// Initializes static members of the class. @@ -42,15 +67,51 @@ namespace Avalonia.Controls set => SetValue(PageTransitionProperty, value); } + /// + /// Gets or sets whether swipe gestures are enabled for navigating between pages. + /// When enabled, mouse pointer events are also accepted in addition to touch and pen. + /// + public bool IsSwipeEnabled + { + get => GetValue(IsSwipeEnabledProperty); + set => SetValue(IsSwipeEnabledProperty, value); + } + + /// + /// Gets or sets the fraction of the viewport occupied by each page. + /// A value of 1 shows a single full page; values below 1 reveal adjacent pages. + /// + public double ViewportFraction + { + get => GetValue(ViewportFractionProperty); + set => SetValue(ViewportFractionProperty, value); + } + + /// + /// Gets a value indicating whether a swipe gesture is currently in progress. + /// + public bool IsSwiping + { + get => _isSwiping; + internal set => SetAndRaise(IsSwipingProperty, ref _isSwiping, value); + } + /// /// Moves to the next item in the carousel. /// public void Next() { + if (ItemCount == 0) + return; + if (SelectedIndex < ItemCount - 1) { ++SelectedIndex; } + else if (WrapSelection) + { + SelectedIndex = 0; + } } /// @@ -58,18 +119,78 @@ namespace Avalonia.Controls /// public void Previous() { + if (ItemCount == 0) + return; + if (SelectedIndex > 0) { --SelectedIndex; } + else if (WrapSelection) + { + SelectedIndex = ItemCount - 1; + } + } + + internal PageSlide.SlideAxis? GetTransitionAxis() + { + var transition = PageTransition; + + if (transition is CompositePageTransition composite) + { + foreach (var t in composite.PageTransitions) + { + if (t is PageSlide slide) + return slide.Orientation; + } + + return null; + } + + return transition is PageSlide ps ? ps.Orientation : null; + } + + internal PageSlide.SlideAxis GetLayoutAxis() => GetTransitionAxis() ?? PageSlide.SlideAxis.Horizontal; + + protected override void OnKeyDown(KeyEventArgs e) + { + base.OnKeyDown(e); + + if (e.Handled || ItemCount == 0) + return; + + var axis = ViewportFraction != 1d ? GetLayoutAxis() : GetTransitionAxis(); + var isVertical = axis == PageSlide.SlideAxis.Vertical; + var isHorizontal = axis == PageSlide.SlideAxis.Horizontal; + + switch (e.Key) + { + case Key.Left when !isVertical: + case Key.Up when !isHorizontal: + Previous(); + e.Handled = true; + break; + case Key.Right when !isVertical: + case Key.Down when !isHorizontal: + Next(); + e.Handled = true; + break; + case Key.Home: + SelectedIndex = 0; + e.Handled = true; + break; + case Key.End: + SelectedIndex = ItemCount - 1; + e.Handled = true; + break; + } } protected override Size ArrangeOverride(Size finalSize) { var result = base.ArrangeOverride(finalSize); - if (_scroller is not null) - _scroller.Offset = new(SelectedIndex, 0); + SyncScrollOffset(); return result; } @@ -78,17 +199,63 @@ namespace Avalonia.Controls { base.OnApplyTemplate(e); _scroller = e.NameScope.Find("PART_ScrollViewer"); + + if (ItemsPanelRoot is VirtualizingCarouselPanel panel) + panel.RefreshGestureRecognizer(); } protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change) { base.OnPropertyChanged(change); - if (change.Property == SelectedIndexProperty && _scroller is not null) + if (change.Property == SelectedIndexProperty) { - var value = change.GetNewValue(); - _scroller.Offset = new(value, 0); + SyncScrollOffset(); } + + if (change.Property == IsSwipeEnabledProperty || + change.Property == PageTransitionProperty || + change.Property == ViewportFractionProperty || + change.Property == WrapSelectionProperty) + { + if (ItemsPanelRoot is VirtualizingCarouselPanel panel) + { + if (change.Property == ViewportFractionProperty && !panel.IsManagingInteractionOffset) + panel.SyncSelectionOffset(SelectedIndex); + + panel.RefreshGestureRecognizer(); + panel.InvalidateMeasure(); + } + + SyncScrollOffset(); + } + } + + private void SyncScrollOffset() + { + if (ItemsPanelRoot is VirtualizingCarouselPanel panel) + { + if (panel.IsManagingInteractionOffset) + return; + + panel.SyncSelectionOffset(SelectedIndex); + + if (ViewportFraction != 1d) + return; + } + + if (_scroller is null) + return; + + _scroller.Offset = CreateScrollOffset(SelectedIndex); + } + + private Vector CreateScrollOffset(int index) + { + if (ViewportFraction != 1d && GetLayoutAxis() == PageSlide.SlideAxis.Vertical) + return new(0, index); + + return new(index, 0); } } } diff --git a/src/Avalonia.Controls/Chrome/CaptionButtons.cs b/src/Avalonia.Controls/Chrome/CaptionButtons.cs deleted file mode 100644 index 32bdd8fa96..0000000000 --- a/src/Avalonia.Controls/Chrome/CaptionButtons.cs +++ /dev/null @@ -1,187 +0,0 @@ -using System; -using Avalonia.Reactive; -using Avalonia.Controls.Metadata; -using Avalonia.Controls.Primitives; - -namespace Avalonia.Controls.Chrome -{ - /// - /// Draws window minimize / maximize / close buttons in a when managed client decorations are enabled. - /// - [TemplatePart(PART_CloseButton, typeof(Button))] - [TemplatePart(PART_RestoreButton, typeof(Button))] - [TemplatePart(PART_MinimizeButton, typeof(Button))] - [TemplatePart(PART_FullScreenButton, typeof(Button))] - [PseudoClasses(":minimized", ":normal", ":maximized", ":fullscreen")] - public class CaptionButtons : TemplatedControl - { - internal const string PART_CloseButton = "PART_CloseButton"; - internal const string PART_RestoreButton = "PART_RestoreButton"; - internal const string PART_MinimizeButton = "PART_MinimizeButton"; - internal const string PART_FullScreenButton = "PART_FullScreenButton"; - - private Button? _restoreButton; - private Button? _minimizeButton; - private Button? _fullScreenButton; - private IDisposable? _disposables; - - /// - /// Currently attached window. - /// - protected Window? HostWindow { get; private set; } - - public virtual void Attach(Window hostWindow) - { - if (_disposables == null) - { - HostWindow = hostWindow; - - _disposables = new CompositeDisposable - { - HostWindow.GetObservable(Window.CanMaximizeProperty) - .Subscribe(_ => - { - UpdateRestoreButtonState(); - UpdateFullScreenButtonState(); - }), - HostWindow.GetObservable(Window.CanMinimizeProperty) - .Subscribe(_ => - { - UpdateMinimizeButtonState(); - }), - HostWindow.GetObservable(Window.WindowStateProperty) - .Subscribe(x => - { - PseudoClasses.Set(":minimized", x == WindowState.Minimized); - PseudoClasses.Set(":normal", x == WindowState.Normal); - PseudoClasses.Set(":maximized", x == WindowState.Maximized); - PseudoClasses.Set(":fullscreen", x == WindowState.FullScreen); - UpdateRestoreButtonState(); - UpdateMinimizeButtonState(); - UpdateFullScreenButtonState(); - }), - }; - } - } - - public virtual void Detach() - { - if (_disposables != null) - { - _disposables.Dispose(); - _disposables = null; - - HostWindow = null; - } - } - - protected virtual void OnClose() - { - HostWindow?.Close(); - } - - protected virtual void OnRestore() - { - if (HostWindow != null) - { - HostWindow.WindowState = HostWindow.WindowState == WindowState.Maximized ? WindowState.Normal : WindowState.Maximized; - } - } - - protected virtual void OnMinimize() - { - if (HostWindow != null) - { - HostWindow.WindowState = WindowState.Minimized; - } - } - - protected virtual void OnToggleFullScreen() - { - if (HostWindow != null) - { - HostWindow.WindowState = HostWindow.WindowState == WindowState.FullScreen - ? WindowState.Normal - : WindowState.FullScreen; - } - } - - protected override void OnApplyTemplate(TemplateAppliedEventArgs e) - { - base.OnApplyTemplate(e); - - if (e.NameScope.Find public static readonly StyledProperty PlaceholderTextProperty = - AvaloniaProperty.Register(nameof(PlaceholderText)); + TextBox.PlaceholderTextProperty.AddOwner(); /// /// Defines the property. /// public static readonly StyledProperty PlaceholderForegroundProperty = - AvaloniaProperty.Register(nameof(PlaceholderForeground)); + TextBox.PlaceholderForegroundProperty.AddOwner(); /// /// Defines the property. @@ -453,7 +453,7 @@ namespace Avalonia.Controls return new ComboBoxAutomationPeer(this); } - protected override void OnGotFocus(GotFocusEventArgs e) + protected override void OnGotFocus(FocusChangedEventArgs e) { if (IsEditable && _inputTextBox != null) { diff --git a/src/Avalonia.Controls/CommandBar/AppBarButton.cs b/src/Avalonia.Controls/CommandBar/AppBarButton.cs new file mode 100644 index 0000000000..9e251f77bf --- /dev/null +++ b/src/Avalonia.Controls/CommandBar/AppBarButton.cs @@ -0,0 +1,119 @@ +namespace Avalonia.Controls +{ + /// + /// A button for use in a . + /// + public class AppBarButton : Button, ICommandBarElement + { + static AppBarButton() + { + ForegroundProperty.Changed.AddClassHandler((x, _) => x.UpdateIconForeground()); + IconProperty.Changed.AddClassHandler((x, _) => x.UpdateIconForeground()); + } + + /// + /// Defines the property. + /// + public static readonly StyledProperty LabelProperty = + AvaloniaProperty.Register(nameof(Label)); + + /// + /// Defines the property. + /// + public static readonly StyledProperty IconProperty = + AvaloniaProperty.Register(nameof(Icon)); + + /// + /// Defines the property. + /// + public static readonly StyledProperty IsCompactProperty = + AvaloniaProperty.Register(nameof(IsCompact)); + + /// + /// Defines the property. + /// + public static readonly StyledProperty DynamicOverflowOrderProperty = + AvaloniaProperty.Register(nameof(DynamicOverflowOrder)); + + /// + /// Defines the property. + /// + public static readonly StyledProperty LabelPositionProperty = + AvaloniaProperty.Register(nameof(LabelPosition), CommandBarDefaultLabelPosition.Bottom); + + /// + /// Defines the property. + /// + public static readonly StyledProperty IsInOverflowProperty = + AvaloniaProperty.Register(nameof(IsInOverflow)); + + /// + /// Gets or sets the text label for the button. + /// + public string? Label + { + get => GetValue(LabelProperty); + set => SetValue(LabelProperty, value); + } + + /// + /// Gets or sets the icon content for the button. + /// + public object? Icon + { + get => GetValue(IconProperty); + set => SetValue(IconProperty, value); + } + + /// + /// Gets or sets whether the button is in compact mode (icon only, label hidden). + /// + public bool IsCompact + { + get => GetValue(IsCompactProperty); + set => SetValue(IsCompactProperty, value); + } + + /// + /// Gets or sets the order in which this button moves to the overflow menu when space is limited. + /// Lower values have higher priority (stay visible longer). + /// + public int DynamicOverflowOrder + { + get => GetValue(DynamicOverflowOrderProperty); + set => SetValue(DynamicOverflowOrderProperty, value); + } + + /// + /// Gets or sets the label position. This is set automatically by the parent . + /// + public CommandBarDefaultLabelPosition LabelPosition + { + get => GetValue(LabelPositionProperty); + set => SetValue(LabelPositionProperty, value); + } + + /// + /// Gets or sets whether this button is displayed inside the overflow popup. + /// Set automatically by when moving items between primary and overflow. + /// + public bool IsInOverflow + { + get => GetValue(IsInOverflowProperty); + set => SetValue(IsInOverflowProperty, value); + } + + private void UpdateIconForeground() + { + if (Icon is IconElement icon) + { + var fg = Foreground; + + if (fg != null) + icon.SetValue(ForegroundProperty, fg); + else + icon.ClearValue(ForegroundProperty); + } + } + } +} diff --git a/src/Avalonia.Controls/CommandBar/AppBarSeparator.cs b/src/Avalonia.Controls/CommandBar/AppBarSeparator.cs new file mode 100644 index 0000000000..53c114528f --- /dev/null +++ b/src/Avalonia.Controls/CommandBar/AppBarSeparator.cs @@ -0,0 +1,41 @@ +using Avalonia.Controls.Primitives; + +namespace Avalonia.Controls +{ + /// + /// A visual separator for use in a . + /// + public class AppBarSeparator : TemplatedControl, ICommandBarElement + { + /// + /// Defines the property. + /// + public static readonly StyledProperty IsCompactProperty = + AvaloniaProperty.Register(nameof(IsCompact)); + + /// + /// Defines the property. + /// + public static readonly StyledProperty IsInOverflowProperty = + AvaloniaProperty.Register(nameof(IsInOverflow)); + + /// + /// Gets or sets whether the separator is in compact mode. + /// + public bool IsCompact + { + get => GetValue(IsCompactProperty); + set => SetValue(IsCompactProperty, value); + } + + /// + /// Gets or sets whether the separator is displayed inside the overflow popup. + /// Set automatically by when moving items between primary and overflow. + /// + public bool IsInOverflow + { + get => GetValue(IsInOverflowProperty); + set => SetValue(IsInOverflowProperty, value); + } + } +} diff --git a/src/Avalonia.Controls/CommandBar/AppBarToggleButton.cs b/src/Avalonia.Controls/CommandBar/AppBarToggleButton.cs new file mode 100644 index 0000000000..2476e3cb93 --- /dev/null +++ b/src/Avalonia.Controls/CommandBar/AppBarToggleButton.cs @@ -0,0 +1,121 @@ +using Avalonia.Controls.Primitives; + +namespace Avalonia.Controls +{ + /// + /// A toggle button for use in a . + /// + public class AppBarToggleButton : ToggleButton, ICommandBarElement + { + static AppBarToggleButton() + { + ForegroundProperty.Changed.AddClassHandler((x, _) => x.UpdateIconForeground()); + IconProperty.Changed.AddClassHandler((x, _) => x.UpdateIconForeground()); + } + + /// + /// Defines the property. + /// + public static readonly StyledProperty LabelProperty = + AvaloniaProperty.Register(nameof(Label)); + + /// + /// Defines the property. + /// + public static readonly StyledProperty IconProperty = + AvaloniaProperty.Register(nameof(Icon)); + + /// + /// Defines the property. + /// + public static readonly StyledProperty IsCompactProperty = + AvaloniaProperty.Register(nameof(IsCompact)); + + /// + /// Defines the property. + /// + public static readonly StyledProperty DynamicOverflowOrderProperty = + AvaloniaProperty.Register(nameof(DynamicOverflowOrder)); + + /// + /// Defines the property. + /// + public static readonly StyledProperty LabelPositionProperty = + AvaloniaProperty.Register(nameof(LabelPosition), CommandBarDefaultLabelPosition.Bottom); + + /// + /// Defines the property. + /// + public static readonly StyledProperty IsInOverflowProperty = + AvaloniaProperty.Register(nameof(IsInOverflow)); + + /// + /// Gets or sets the text label for the button. + /// + public string? Label + { + get => GetValue(LabelProperty); + set => SetValue(LabelProperty, value); + } + + /// + /// Gets or sets the icon content for the button. + /// + public object? Icon + { + get => GetValue(IconProperty); + set => SetValue(IconProperty, value); + } + + /// + /// Gets or sets whether the button is in compact mode (icon only, label hidden). + /// + public bool IsCompact + { + get => GetValue(IsCompactProperty); + set => SetValue(IsCompactProperty, value); + } + + /// + /// Gets or sets the order in which this button moves to the overflow menu when space is limited. + /// Lower values have higher priority (stay visible longer). + /// + public int DynamicOverflowOrder + { + get => GetValue(DynamicOverflowOrderProperty); + set => SetValue(DynamicOverflowOrderProperty, value); + } + + /// + /// Gets or sets the label position. This is set automatically by the parent . + /// + public CommandBarDefaultLabelPosition LabelPosition + { + get => GetValue(LabelPositionProperty); + set => SetValue(LabelPositionProperty, value); + } + + /// + /// Gets or sets whether this button is displayed inside the overflow popup. + /// Set automatically by when moving items between primary and overflow. + /// + public bool IsInOverflow + { + get => GetValue(IsInOverflowProperty); + set => SetValue(IsInOverflowProperty, value); + } + + private void UpdateIconForeground() + { + if (Icon is IconElement icon) + { + var fg = Foreground; + + if (fg != null) + icon.SetValue(ForegroundProperty, fg); + else + icon.ClearValue(ForegroundProperty); + } + } + } +} diff --git a/src/Avalonia.Controls/CommandBar/CommandBar.cs b/src/Avalonia.Controls/CommandBar/CommandBar.cs new file mode 100644 index 0000000000..392ffb7ddc --- /dev/null +++ b/src/Avalonia.Controls/CommandBar/CommandBar.cs @@ -0,0 +1,640 @@ +using System; +using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.Collections.Specialized; +using System.Diagnostics.CodeAnalysis; +using Avalonia.Controls.Metadata; +using Avalonia.Controls.Primitives; +using Avalonia.Interactivity; +using Avalonia.Metadata; + +namespace Avalonia.Controls +{ + /// + /// A command bar that provides primary commands displayed inline and secondary commands + /// accessible via an overflow menu. + /// + [TemplatePart("PART_OverflowButton", typeof(Button))] + [TemplatePart("PART_OverflowPopup", typeof(Popup))] + [TemplatePart("PART_ContentPresenter", typeof(Control))] + public class CommandBar : TemplatedControl + { + /// + /// Defines the property. + /// + public static readonly StyledProperty?> PrimaryCommandsProperty = + AvaloniaProperty.Register?>(nameof(PrimaryCommands)); + + /// + /// Defines the property. + /// + public static readonly StyledProperty?> SecondaryCommandsProperty = + AvaloniaProperty.Register?>(nameof(SecondaryCommands)); + + /// + /// Defines the property. + /// + public static readonly StyledProperty ContentProperty = + ContentControl.ContentProperty.AddOwner(); + + /// + /// Defines the property. + /// + public static readonly StyledProperty DefaultLabelPositionProperty = + AvaloniaProperty.Register(nameof(DefaultLabelPosition), CommandBarDefaultLabelPosition.Bottom); + + /// + /// Defines the property. + /// + public static readonly StyledProperty IsDynamicOverflowEnabledProperty = + AvaloniaProperty.Register(nameof(IsDynamicOverflowEnabled)); + + /// + /// Defines the property. + /// + public static readonly StyledProperty OverflowButtonVisibilityProperty = + AvaloniaProperty.Register(nameof(OverflowButtonVisibility), CommandBarOverflowButtonVisibility.Auto); + + /// + /// Defines the property. + /// + public static readonly StyledProperty IsOpenProperty = + AvaloniaProperty.Register(nameof(IsOpen)); + + /// + /// Defines the property. + /// + public static readonly StyledProperty IsStickyProperty = + AvaloniaProperty.Register(nameof(IsSticky)); + + /// + /// Defines the property. + /// + public static readonly StyledProperty ItemWidthBottomProperty = + AvaloniaProperty.Register(nameof(ItemWidthBottom), defaultValue: 70d); + + /// + /// Defines the property. + /// + public static readonly StyledProperty ItemWidthRightProperty = + AvaloniaProperty.Register(nameof(ItemWidthRight), defaultValue: 102d); + + /// + /// Defines the property. + /// + public static readonly StyledProperty ItemWidthCollapsedProperty = + AvaloniaProperty.Register(nameof(ItemWidthCollapsed), defaultValue: 42d); + + private bool _hasSecondaryCommands; + /// + /// Defines the property. + /// + public static readonly DirectProperty HasSecondaryCommandsProperty = + AvaloniaProperty.RegisterDirect( + nameof(HasSecondaryCommands), + o => o._hasSecondaryCommands); + + private bool _isOverflowButtonVisible; + /// + /// Defines the property. + /// + public static readonly DirectProperty IsOverflowButtonVisibleProperty = + AvaloniaProperty.RegisterDirect( + nameof(IsOverflowButtonVisible), + o => o._isOverflowButtonVisible); + + /// + /// Defines the routed event. + /// + public static readonly RoutedEvent OpeningEvent = + RoutedEvent.Register(nameof(Opening), RoutingStrategies.Bubble); + + /// + /// Defines the routed event. + /// + public static readonly RoutedEvent OpenedEvent = + RoutedEvent.Register(nameof(Opened), RoutingStrategies.Bubble); + + /// + /// Defines the routed event. + /// + public static readonly RoutedEvent ClosingEvent = + RoutedEvent.Register(nameof(Closing), RoutingStrategies.Bubble); + + /// + /// Defines the routed event. + /// + public static readonly RoutedEvent ClosedEvent = + RoutedEvent.Register(nameof(Closed), RoutingStrategies.Bubble); + + private Button? _overflowButton; + private Popup? _overflowPopup; + private Control? _contentPresenter; + + private readonly ObservableCollection _visiblePrimaryCommands = new(); + private readonly ObservableCollection _overflowItems = new(); + private bool _isDynamicUpdateInProgress; + private double _constraintWidth = double.PositiveInfinity; + + public CommandBar() + { + VisiblePrimaryCommands = new ReadOnlyObservableCollection(_visiblePrimaryCommands); + OverflowItems = new ReadOnlyObservableCollection(_overflowItems); + + var primaryCommands = new ObservableCollection(); + SetCurrentValue(PrimaryCommandsProperty, (IList)primaryCommands); + + var secondaryCommands = new ObservableCollection(); + SetCurrentValue(SecondaryCommandsProperty, (IList)secondaryCommands); + + SizeChanged += CommandBar_SizeChanged; + } + + /// + /// Gets the read-only collection of primary commands currently visible in the bar + /// (may be a subset when dynamic overflow is active). + /// + public ReadOnlyObservableCollection VisiblePrimaryCommands { get; } + + /// + /// Gets the read-only collection of items shown in the overflow popup (secondary commands + /// plus any primary commands moved to overflow by dynamic overflow). + /// + public ReadOnlyObservableCollection OverflowItems { get; } + + /// + /// Gets or sets the collection of primary commands displayed in the bar. + /// + [Content] + [SuppressMessage("AvaloniaProperty", "AVP1030:StyledProperty accessors should not have side effects", Justification = "Necessary for now to avoid returning null.")] + public IList PrimaryCommands + { + get + { + var value = GetValue(PrimaryCommandsProperty); + if (value is null) + { + var list = new ObservableCollection(); + SetCurrentValue(PrimaryCommandsProperty, (IList)list); + return list; + } + return value; + } + set => SetValue(PrimaryCommandsProperty, value); + } + + /// + /// Gets or sets the collection of secondary commands shown in the overflow menu. + /// + [SuppressMessage("AvaloniaProperty", "AVP1030:StyledProperty accessors should not have side effects", Justification = "Necessary for now to avoid returning null.")] + public IList SecondaryCommands + { + get + { + var value = GetValue(SecondaryCommandsProperty); + if (value is null) + { + var list = new ObservableCollection(); + SetCurrentValue(SecondaryCommandsProperty, (IList)list); + return list; + } + return value; + } + set => SetValue(SecondaryCommandsProperty, value); + } + + /// + /// Gets or sets custom content displayed at the start (left) of the bar. + /// + public object? Content + { + get => GetValue(ContentProperty); + set => SetValue(ContentProperty, value); + } + + /// + /// Gets or sets how labels are positioned on command buttons. + /// + public CommandBarDefaultLabelPosition DefaultLabelPosition + { + get => GetValue(DefaultLabelPositionProperty); + set => SetValue(DefaultLabelPositionProperty, value); + } + + /// + /// Gets or sets whether primary commands are automatically moved to the overflow menu + /// when there is not enough space to display them. + /// + public bool IsDynamicOverflowEnabled + { + get => GetValue(IsDynamicOverflowEnabledProperty); + set => SetValue(IsDynamicOverflowEnabledProperty, value); + } + + /// + /// Gets or sets the visibility of the overflow button. + /// + public CommandBarOverflowButtonVisibility OverflowButtonVisibility + { + get => GetValue(OverflowButtonVisibilityProperty); + set => SetValue(OverflowButtonVisibilityProperty, value); + } + + /// + /// Gets or sets whether the overflow menu is open. + /// + public bool IsOpen + { + get => GetValue(IsOpenProperty); + set => SetValue(IsOpenProperty, value); + } + + /// + /// Gets or sets whether the overflow menu stays open after a command is invoked + /// (disables light-dismiss behavior). + /// + public bool IsSticky + { + get => GetValue(IsStickyProperty); + set => SetValue(IsStickyProperty, value); + } + + /// + /// Gets or sets the estimated item width used in dynamic-overflow calculations + /// when is . + /// + public double ItemWidthBottom + { + get => GetValue(ItemWidthBottomProperty); + set => SetValue(ItemWidthBottomProperty, value); + } + + /// + /// Gets or sets the estimated item width used in dynamic-overflow calculations + /// when is . + /// + public double ItemWidthRight + { + get => GetValue(ItemWidthRightProperty); + set => SetValue(ItemWidthRightProperty, value); + } + + /// + /// Gets or sets the estimated item width used in dynamic-overflow calculations + /// when is . + /// + public double ItemWidthCollapsed + { + get => GetValue(ItemWidthCollapsedProperty); + set => SetValue(ItemWidthCollapsedProperty, value); + } + + /// + /// Gets whether there are any commands (secondary or overflowed primary) in the overflow menu. + /// + public bool HasSecondaryCommands + { + get => _hasSecondaryCommands; + private set => SetAndRaise(HasSecondaryCommandsProperty, ref _hasSecondaryCommands, value); + } + + /// + /// Gets whether the overflow button is currently visible. + /// + public bool IsOverflowButtonVisible + { + get => _isOverflowButtonVisible; + private set => SetAndRaise(IsOverflowButtonVisibleProperty, ref _isOverflowButtonVisible, value); + } + + /// + /// Occurs when the overflow menu is about to open. + /// + public event EventHandler? Opening + { + add => AddHandler(OpeningEvent, value); + remove => RemoveHandler(OpeningEvent, value); + } + + /// + /// Occurs when the overflow menu has opened. + /// + public event EventHandler? Opened + { + add => AddHandler(OpenedEvent, value); + remove => RemoveHandler(OpenedEvent, value); + } + + /// + /// Occurs when the overflow menu is about to close. + /// + public event EventHandler? Closing + { + add => AddHandler(ClosingEvent, value); + remove => RemoveHandler(ClosingEvent, value); + } + + /// + /// Occurs when the overflow menu has closed. + /// + public event EventHandler? Closed + { + add => AddHandler(ClosedEvent, value); + remove => RemoveHandler(ClosedEvent, value); + } + + protected override void OnApplyTemplate(TemplateAppliedEventArgs e) + { + base.OnApplyTemplate(e); + + if (_overflowButton != null) + _overflowButton.Click -= OnOverflowButtonClick; + + _overflowButton = e.NameScope.Find