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 d1463ad26b..2d648aa7ba 100644 --- a/.gitmodules +++ b/.gitmodules @@ -1,6 +1,3 @@ -[submodule "Numerge"] - path = external/Numerge - url = https://github.com/kekekeks/Numerge.git [submodule "XamlX"] path = external/XamlX url = https://github.com/kekekeks/XamlX.git diff --git a/Directory.Packages.props b/Directory.Packages.props index 58650e1729..9441c68cac 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -36,6 +36,7 @@ + diff --git a/api/Avalonia.LinuxFramebuffer.nupkg.xml b/api/Avalonia.LinuxFramebuffer.nupkg.xml index 10c927a203..0fa6ef4e03 100644 --- a/api/Avalonia.LinuxFramebuffer.nupkg.xml +++ b/api/Avalonia.LinuxFramebuffer.nupkg.xml @@ -1,4 +1,4 @@ - + @@ -37,4 +37,4 @@ baseline/Avalonia.LinuxFramebuffer/lib/net8.0/Avalonia.LinuxFramebuffer.dll current/Avalonia.LinuxFramebuffer/lib/net8.0/Avalonia.LinuxFramebuffer.dll - \ No newline at end of file + diff --git a/api/Avalonia.Skia.nupkg.xml b/api/Avalonia.Skia.nupkg.xml index c1afe2f966..b73745af8e 100644 --- a/api/Avalonia.Skia.nupkg.xml +++ b/api/Avalonia.Skia.nupkg.xml @@ -1,4 +1,4 @@ - + @@ -169,4 +169,4 @@ baseline/Avalonia.Skia/lib/net8.0/Avalonia.Skia.dll current/Avalonia.Skia/lib/net8.0/Avalonia.Skia.dll - \ No newline at end of file + diff --git a/api/Avalonia.nupkg.xml b/api/Avalonia.nupkg.xml index 92f41c6606..dd20d0f39e 100644 --- a/api/Avalonia.nupkg.xml +++ b/api/Avalonia.nupkg.xml @@ -1,4 +1,4 @@ - + @@ -109,6 +109,12 @@ 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 @@ -157,6 +163,12 @@ 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 @@ -397,6 +409,12 @@ 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 @@ -571,6 +589,12 @@ 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 @@ -619,6 +643,12 @@ 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 @@ -859,6 +889,12 @@ 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 @@ -961,6 +997,18 @@ 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 @@ -1069,12 +1117,48 @@ baseline/Avalonia/lib/net10.0/Avalonia.Base.dll current/Avalonia/lib/net10.0/Avalonia.Base.dll + + CP0002 + 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.FocusManager.ClearFocus + baseline/Avalonia/lib/net10.0/Avalonia.Base.dll + current/Avalonia/lib/net10.0/Avalonia.Base.dll + + + CP0002 + 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.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.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.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.IFocusManager.ClearFocus + baseline/Avalonia/lib/net10.0/Avalonia.Base.dll + current/Avalonia/lib/net10.0/Avalonia.Base.dll + CP0002 M:Avalonia.Input.IInputRoot.get_KeyboardNavigationHandler @@ -1177,6 +1261,18 @@ baseline/Avalonia/lib/net10.0/Avalonia.Base.dll current/Avalonia/lib/net10.0/Avalonia.Base.dll + + CP0002 + 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.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.Input.InputElement.RemovePinchEndedHandler(Avalonia.Interactivity.Interactive,System.EventHandler{Avalonia.Input.PinchEndedEventArgs}) @@ -1375,6 +1471,12 @@ 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) @@ -1555,6 +1657,42 @@ 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) @@ -1567,6 +1705,30 @@ 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}}}) @@ -1855,6 +2017,12 @@ 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 @@ -2359,6 +2527,18 @@ baseline/Avalonia/lib/net8.0/Avalonia.Base.dll current/Avalonia/lib/net8.0/Avalonia.Base.dll + + CP0002 + F:Avalonia.Input.InputElement.GotFocusEvent + baseline/Avalonia/lib/net8.0/Avalonia.Base.dll + current/Avalonia/lib/net8.0/Avalonia.Base.dll + + + CP0002 + F:Avalonia.Input.InputElement.LostFocusEvent + baseline/Avalonia/lib/net8.0/Avalonia.Base.dll + current/Avalonia/lib/net8.0/Avalonia.Base.dll + CP0002 F:Avalonia.Media.DrawingImage.ViewboxProperty @@ -2467,12 +2647,48 @@ baseline/Avalonia/lib/net8.0/Avalonia.Base.dll current/Avalonia/lib/net8.0/Avalonia.Base.dll + + CP0002 + 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.Input.FocusManager.ClearFocus + baseline/Avalonia/lib/net8.0/Avalonia.Base.dll + current/Avalonia/lib/net8.0/Avalonia.Base.dll + + + CP0002 + 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.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.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.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 @@ -2575,6 +2791,18 @@ baseline/Avalonia/lib/net8.0/Avalonia.Base.dll current/Avalonia/lib/net8.0/Avalonia.Base.dll + + CP0002 + 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.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.Input.InputElement.RemovePinchEndedHandler(Avalonia.Interactivity.Interactive,System.EventHandler{Avalonia.Input.PinchEndedEventArgs}) @@ -2773,6 +3001,12 @@ baseline/Avalonia/lib/net8.0/Avalonia.Base.dll current/Avalonia/lib/net8.0/Avalonia.Base.dll + + CP0002 + 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.Media.Fonts.IFontCollection.Initialize(Avalonia.Platform.IFontManagerImpl) @@ -2953,6 +3187,42 @@ baseline/Avalonia/lib/net8.0/Avalonia.Base.dll current/Avalonia/lib/net8.0/Avalonia.Base.dll + + CP0002 + 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.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.Rendering.DefaultRenderTimer.Start + baseline/Avalonia/lib/net8.0/Avalonia.Base.dll + current/Avalonia/lib/net8.0/Avalonia.Base.dll + + + CP0002 + M:Avalonia.Rendering.DefaultRenderTimer.Stop + baseline/Avalonia/lib/net8.0/Avalonia.Base.dll + current/Avalonia/lib/net8.0/Avalonia.Base.dll + + + CP0002 + 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.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.Rendering.SceneInvalidatedEventArgs.#ctor(Avalonia.Rendering.IRenderRoot,Avalonia.Rect) @@ -2965,6 +3235,30 @@ baseline/Avalonia/lib/net8.0/Avalonia.Base.dll current/Avalonia/lib/net8.0/Avalonia.Base.dll + + CP0002 + 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.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.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.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.Utilities.AvaloniaResourcesIndexReaderWriter.WriteResources(System.IO.Stream,System.Collections.Generic.List{System.ValueTuple{System.String,System.Int32,System.Func{System.IO.Stream}}}) @@ -3253,6 +3547,12 @@ 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 @@ -3787,6 +4087,48 @@ 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}) @@ -3883,6 +4225,18 @@ 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 @@ -4051,6 +4405,48 @@ 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.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 + 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 M:Avalonia.Input.IKeyboardNavigationHandler.Move(Avalonia.Input.IInputElement,Avalonia.Input.NavigationDirection,Avalonia.Input.KeyModifiers,System.Nullable{Avalonia.Input.KeyDeviceType}) @@ -4183,6 +4579,18 @@ 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 @@ -4969,4 +5377,4 @@ baseline/Avalonia/lib/netstandard2.0/Avalonia.Base.dll current/Avalonia/lib/netstandard2.0/Avalonia.Base.dll - \ No newline at end of file + 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/external/Numerge b/external/Numerge deleted file mode 160000 index 5530e1cbe9..0000000000 --- a/external/Numerge +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 5530e1cbe9e105ff4ebc9da1f4af3253a8756754 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/samples/ControlCatalog/MainView.xaml b/samples/ControlCatalog/MainView.xaml index c8c496b50c..b6249fe17f 100644 --- a/samples/ControlCatalog/MainView.xaml +++ b/samples/ControlCatalog/MainView.xaml @@ -161,6 +161,9 @@ + + + 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/DialogsPage.xaml.cs b/samples/ControlCatalog/Pages/DialogsPage.xaml.cs index 892e320afc..adcf844552 100644 --- a/samples/ControlCatalog/Pages/DialogsPage.xaml.cs +++ b/samples/ControlCatalog/Pages/DialogsPage.xaml.cs @@ -228,13 +228,8 @@ namespace ControlCatalog.Pages try { // Sync disposal of StreamWriter is not supported on WASM -#if NET6_0_OR_GREATER await using var stream = await file.OpenWriteAsync(); await using var writer = new System.IO.StreamWriter(stream); -#else - using var stream = await file.OpenWriteAsync(); - using var writer = new System.IO.StreamWriter(stream); -#endif await writer.WriteLineAsync(openedFileContent.Text); SetFolder(await file.GetParentAsync()); @@ -265,13 +260,8 @@ namespace ControlCatalog.Pages if (result.File is { } file) { // Sync disposal of StreamWriter is not supported on WASM -#if NET6_0_OR_GREATER await using var stream = await file.OpenWriteAsync(); await using var writer = new System.IO.StreamWriter(stream); -#else - using var stream = await file.OpenWriteAsync(); - using var writer = new System.IO.StreamWriter(stream); -#endif if (result.SelectedFileType == FilePickerFileTypes.Xml) { await writer.WriteLineAsync("Test"); @@ -431,11 +421,7 @@ namespace ControlCatalog.Pages internal static async Task ReadTextFromFile(IStorageFile file, int length) { -#if NET6_0_OR_GREATER await using var stream = await file.OpenReadAsync(); -#else - using var stream = await file.OpenReadAsync(); -#endif using var reader = new System.IO.StreamReader(stream); // 4GB file test, shouldn't load more than 10000 chars into a memory. 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..8f27cc61f8 --- /dev/null +++ b/samples/ControlCatalog/Pages/PipsPagerPage.xaml.cs @@ -0,0 +1,47 @@ +using System; +using Avalonia.Controls; +using Avalonia.Interactivity; + +namespace ControlCatalog.Pages +{ + public partial class PipsPagerPage : UserControl + { + private static readonly (string Group, string Title, string Description, Func Factory)[] Demos = + { + ("Getting Started", "First Look", + "Default PipsPager with horizontal and vertical orientation, with and without navigation buttons.", + () => new PipsPagerGettingStartedPage()), + + ("Features", "Carousel Integration", + "Bind SelectedPageIndex to a Carousel's SelectedIndex for two-way synchronized page navigation.", + () => new PipsPagerCarouselPage()), + ("Features", "Large Collections", + "Use MaxVisiblePips to limit visible indicators when the page count is large. Pips scroll automatically.", + () => new PipsPagerLargeCollectionPage()), + ("Features", "Events", + "Monitor SelectedPageIndex changes to react to user navigation.", + () => new PipsPagerEventsPage()), + + ("Appearance", "Custom Colors", + "Override pip indicator colors using resource keys for normal, selected, and hover states.", + () => new PipsPagerCustomColorsPage()), + ("Appearance", "Custom Buttons", + "Replace the default chevron navigation buttons with custom styled buttons.", + () => new PipsPagerCustomButtonsPage()), + ("Appearance", "Custom Templates", + "Override pip item templates to create squares, pills, numbers, or any custom shape.", + () => new PipsPagerCustomTemplatesPage()), + }; + + public PipsPagerPage() + { + InitializeComponent(); + Loaded += OnLoaded; + } + + private async void OnLoaded(object? sender, RoutedEventArgs e) + { + await SampleNav.PushAsync(NavigationDemoHelper.CreateGalleryHomePage(SampleNav, Demos), null); + } + } +} diff --git a/samples/SampleControls/HamburgerMenu/HamburgerMenu.xaml b/samples/SampleControls/HamburgerMenu/HamburgerMenu.xaml index cffecb5b7d..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"> - + + IsChecked="{Binding #PART_NavigationPane.IsPaneOpen, Mode=TwoWay}" + AutomationProperties.ControlTypeOverride="ListItem"> diff --git a/src/Android/Avalonia.Android/AndroidPlatform.cs b/src/Android/Avalonia.Android/AndroidPlatform.cs index 7a3059cb65..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().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/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/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/Avalonia.Base.csproj b/src/Avalonia.Base/Avalonia.Base.csproj index ffaac716b9..99524857a7 100644 --- a/src/Avalonia.Base/Avalonia.Base.csproj +++ b/src/Avalonia.Base/Avalonia.Base.csproj @@ -16,11 +16,7 @@ - - - - @@ -65,8 +61,6 @@ - + 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/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/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/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/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/FindNextElementOptions.cs b/src/Avalonia.Base/Input/FindNextElementOptions.cs index e6062daf9b..72d83ec419 100644 --- a/src/Avalonia.Base/Input/FindNextElementOptions.cs +++ b/src/Avalonia.Base/Input/FindNextElementOptions.cs @@ -6,12 +6,49 @@ using System.Threading.Tasks; 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 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. + /// public InputElement? SearchRoot { get; init; } + + /// + /// Gets or sets the rectangular region within the visual hierarchy that will be excluded + /// from consideration during focus navigation. + /// 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. + /// 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. + /// 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. + /// 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..ade599ee08 --- /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/FocusManager.cs b/src/Avalonia.Base/Input/FocusManager.cs index 15b8fea77d..dc62171f48 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,58 +40,51 @@ namespace Avalonia.Input RoutingStrategies.Tunnel); } + [PrivateApi] public FocusManager() { - _contentRoot = null; } - public FocusManager(IInputElement contentRoot) - { - _contentRoot = contentRoot; - } - - internal void SetContentRoot(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 && @@ -110,12 +101,7 @@ namespace Avalonia.Input } } - public void ClearFocus() - { - Focus(null); - } - - public void ClearFocusOnElementRemoved(IInputElement removedElement, Visual oldParent) + internal void ClearFocusOnElementRemoved(IInputElement removedElement, Visual oldParent) { if (oldParent is IInputElement parentElement && GetFocusScope(parentElement) is StyledElement scope && @@ -129,6 +115,7 @@ namespace Avalonia.Input Focus(null); } + [PrivateApi] public IInputElement? GetFocusedElement(IFocusScope scope) { return (scope as StyledElement)?.GetValue(FocusedElementProperty); @@ -138,6 +125,7 @@ namespace Avalonia.Input /// Notifies the focus manager of a change in focus scope. /// /// The new focus scope. + [PrivateApi] public void SetFocusScope(IFocusScope scope) { if (GetFocusedElement(scope) is { } focused) @@ -153,12 +141,14 @@ namespace Avalonia.Input } } + [PrivateApi] public void RemoveFocusRoot(IFocusScope scope) { if (scope == _focusRoot) - ClearFocus(); + Focus(null); } + [PrivateApi] public static bool GetIsFocusScope(IInputElement e) => e is IFocusScope; /// @@ -176,25 +166,15 @@ namespace Avalonia.Input ?? (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(direction, focusOptions); + _reusableFocusOptions = focusOptions; + return result; } /// @@ -295,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; @@ -317,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; @@ -339,52 +313,59 @@ 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(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) 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/IFocusManager.cs b/src/Avalonia.Base/Input/IFocusManager.cs index 5691172f3f..9bd1fb4239 100644 --- a/src/Avalonia.Base/Input/IFocusManager.cs +++ b/src/Avalonia.Base/Input/IFocusManager.cs @@ -14,9 +14,66 @@ 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. + /// They only apply to directional navigation. + /// + /// 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. + /// They only apply to directional navigation. + /// + /// 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/InputElement.Gestures.cs b/src/Avalonia.Base/Input/InputElement.Gestures.cs index 1ad1146282..83f350f0e7 100644 --- a/src/Avalonia.Base/Input/InputElement.Gestures.cs +++ b/src/Avalonia.Base/Input/InputElement.Gestures.cs @@ -149,7 +149,7 @@ namespace Avalonia.Input } /// - /// Occurs when a pinch gesture occurs on the control. + /// Occurs when the user moves two contact points closer together. /// public event EventHandler? Pinch { @@ -158,7 +158,7 @@ namespace Avalonia.Input } /// - /// Occurs when a pinch gesture ends on the control. + /// Occurs when the user releases both contact points used in a pinch gesture. /// public event EventHandler? PinchEnded { @@ -167,7 +167,7 @@ namespace Avalonia.Input } /// - /// Occurs when a pull gesture occurs on the control. + /// Occurs when the user drags from the edge of a control. /// public event EventHandler? PullGesture { @@ -176,7 +176,7 @@ namespace Avalonia.Input } /// - /// Occurs when a pull gesture ends on the control. + /// Occurs when the user releases the pointer after a pull gesture. /// public event EventHandler? PullGestureEnded { @@ -185,7 +185,7 @@ namespace Avalonia.Input } /// - /// Occurs when a scroll gesture occurs on the control. + /// Occurs when the user continuously moves the pointer in the same direction within the control’s boundaries. /// public event EventHandler? ScrollGesture { @@ -194,7 +194,7 @@ namespace Avalonia.Input } /// - /// Occurs when a scroll gesture inertia starts on the control. + /// Occurs within a scroll gesture, when the user releases the pointer, and scrolling continues by transitioning to momentum-based gliding movement. /// public event EventHandler? ScrollGestureInertiaStarting { @@ -203,7 +203,7 @@ namespace Avalonia.Input } /// - /// Occurs when a scroll gesture ends on the control. + /// Occurs when a scroll gesture has fully stopped, taking into account any inertial movement that continues the scroll after the user has released the pointer. /// public event EventHandler? ScrollGestureEnded { @@ -212,7 +212,7 @@ namespace Avalonia.Input } /// - /// Occurs when a touchpad magnify gesture occurs on the control. + /// Occurs when the user moves two contact points away from each other on a touchpad. /// public event EventHandler? PointerTouchPadGestureMagnify { @@ -221,7 +221,7 @@ namespace Avalonia.Input } /// - /// Occurs when a touchpad rotate gesture occurs on the control. + /// Occurs when the user places two contact points and moves them in a circular motion on a touchpad. /// public event EventHandler? PointerTouchPadGestureRotate { @@ -230,7 +230,7 @@ namespace Avalonia.Input } /// - /// Occurs when a swipe gesture occurs on the control. + /// Occurs when the user rapidly drags the pointer in a single direction across the control. /// public event EventHandler? SwipeGesture { @@ -239,7 +239,7 @@ namespace Avalonia.Input } /// - /// Occurs when a touchpad swipe gesture occurs on the control. + /// Occurs when the user performs a rapid dragging motion in a single direction on a touchpad. /// public event EventHandler? PointerTouchPadGestureSwipe { @@ -248,7 +248,7 @@ namespace Avalonia.Input } /// - /// Occurs when a tap gesture occurs on the control. + /// Occurs when the user briefly contacts and releases a single point, without significant movement. /// public event EventHandler? Tapped { @@ -257,7 +257,7 @@ namespace Avalonia.Input } /// - /// Occurs when a right tap gesture occurs on the control. + /// Occurs when the user briefly contacts and releases a single point, without significant movement, using a mechanism on the input device recognized as a right button or equivalent. /// public event EventHandler? RightTapped { @@ -266,7 +266,7 @@ namespace Avalonia.Input } /// - /// Occurs when a hold gesture occurs on the control. + /// Occurs when the user makes a single contact, then maintains contact beyond a given time threshold without releasing or making another contact. /// public event EventHandler? Holding { @@ -275,7 +275,7 @@ namespace Avalonia.Input } /// - /// Occurs when a double-tap gesture occurs on the control. + /// Occurs when the user briefly contacts and releases twice on a single point, without significant movement. /// public event EventHandler? DoubleTapped { diff --git a/src/Avalonia.Base/Input/InputElement.cs b/src/Avalonia.Base/Input/InputElement.cs index 98b988f8cd..e908e818e8 100644 --- a/src/Avalonia.Base/Input/InputElement.cs +++ b/src/Avalonia.Base/Input/InputElement.cs @@ -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. @@ -278,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); } @@ -296,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); } @@ -523,7 +523,7 @@ namespace Avalonia.Input if (!IsEffectivelyEnabled && FocusManager.GetFocusManager(this) is { } focusManager && Equals(focusManager.GetFocusedElement(), this)) { - focusManager.ClearFocus(); + focusManager.Focus(null); } } } @@ -593,7 +593,7 @@ namespace Avalonia.Input UpdateIsEffectivelyEnabled(); } - private void OnGotFocusCore(GotFocusEventArgs e) + private void OnGotFocusCore(FocusChangedEventArgs e) { var isFocused = e.Source == this; _isFocusVisible = isFocused && (e.NavigationMethod == NavigationMethod.Directional || e.NavigationMethod == NavigationMethod.Tab); @@ -617,11 +617,11 @@ namespace Avalonia.Input /// for this event. /// /// 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; @@ -634,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) { } @@ -995,7 +995,7 @@ namespace Avalonia.Input } else { - focusManager.ClearFocus(); + focusManager.Focus(null); } } } 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 3d9764528a..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 || + if (oldElement != null && + (!((Visual)oldElement).IsAttachedToVisualTree || _focusedRoot != ((Visual?)element)?.GetInputRoot()) && _focusedRoot != null) { ClearChildrenFocusWithin(_focusedRoot.RootElement, true); } - SetIsFocusWithin(FocusedElement, element); + SetIsFocusWithin(oldElement, element); _focusedElement = element; _focusedRoot = (_focusedElement as Visual)?.GetInputRoot(); - interactive?.RaiseEvent(new RoutedEventArgs(InputElement.LostFocusEvent)); + interactive?.RaiseEvent(new FocusChangedEventArgs(InputElement.LostFocusEvent) + { + OldFocusedElement = oldElement, + NewFocusedElement = element, + NavigationMethod = method, + KeyModifiers = keyModifiers + }); - (element as Interactive)?.RaiseEvent(new GotFocusEventArgs + (element as Interactive)?.RaiseEvent(new FocusChangedEventArgs(InputElement.GotFocusEvent) { + OldFocusedElement = oldElement, + NewFocusedElement = element, NavigationMethod = method, - KeyModifiers = keyModifiers, + KeyModifiers = keyModifiers }); _textInputManager.SetFocusedElement(element); 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/Layout/LayoutHelper.cs b/src/Avalonia.Base/Layout/LayoutHelper.cs index c50053dc05..fd81ffaa49 100644 --- a/src/Avalonia.Base/Layout/LayoutHelper.cs +++ b/src/Avalonia.Base/Layout/LayoutHelper.cs @@ -263,12 +263,7 @@ namespace Avalonia.Layout // point precision error (e.g. 79.333333333333343) then when it's multiplied by // `dpiScale` and rounded up, it will be rounded up to a value one greater than it // should be. -#if NET6_0_OR_GREATER return Math.Round(value, 8, MidpointRounding.ToZero); -#else - // MidpointRounding.ToZero isn't available in netstandard2.0. - return Math.Truncate(value * 1e8) / 1e8; -#endif } } } 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 40176c88ff..6e4283d76e 100644 --- a/src/Avalonia.Base/Media/Fonts/FontCollectionBase.cs +++ b/src/Avalonia.Base/Media/Fonts/FontCollectionBase.cs @@ -168,7 +168,7 @@ namespace Avalonia.Media.Fonts var key = typeface.ToFontCollectionKey(); - return TryGetGlyphTypeface(familyName, key, out glyphTypeface); + return TryGetGlyphTypeface(familyName, key, allowNearestMatch: true, out glyphTypeface); } public virtual bool TryGetFamilyTypefaces(string familyName, [NotNullWhen(true)] out IReadOnlyList? familyTypefaces) @@ -455,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)) { @@ -511,7 +511,7 @@ namespace Avalonia.Media.Fonts { // Exact match found in snapshot. Use the exact family name for lookup if (_glyphTypefaceCache.TryGetValue(snapshot[mid].Name, out var exactGlyphTypefaces) && - TryGetNearestMatch(exactGlyphTypefaces, key, out glyphTypeface)) + TryGetMatch(exactGlyphTypefaces, key, allowNearestMatch, out glyphTypeface, out _)) { return true; } @@ -549,7 +549,7 @@ namespace Avalonia.Media.Fonts } if (_glyphTypefaceCache.TryGetValue(fontFamily.Name, out glyphTypefaces) && - TryGetNearestMatch(glyphTypefaces, key, out glyphTypeface)) + TryGetMatch(glyphTypefaces, key, allowNearestMatch, out glyphTypeface, out _)) { return true; } @@ -559,6 +559,29 @@ namespace Avalonia.Media.Fonts return false; } + private bool TryGetMatch( + IDictionary 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. diff --git a/src/Avalonia.Base/Media/Fonts/SystemFontCollection.cs b/src/Avalonia.Base/Media/Fonts/SystemFontCollection.cs index 3c81e9890f..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,6 +52,13 @@ namespace Avalonia.Media.Fonts return false; } + // The font manager didn't return a perfect match either. Find the nearest match ourselves. + if (key != platformTypeface.ToFontCollectionKey() && + TryGetGlyphTypeface(familyName, key, allowNearestMatch: true, out glyphTypeface)) + { + return true; + } + glyphTypeface = GlyphTypeface.TryCreate(platformTypeface); if (glyphTypeface is null) { @@ -77,7 +84,7 @@ namespace Avalonia.Media.Fonts } //Requested glyph typeface should be in cache now - return base.TryGetGlyphTypeface(familyName, style, weight, stretch, out glyphTypeface); + return TryGetGlyphTypeface(familyName, key, allowNearestMatch: false, out glyphTypeface); } public override bool TryGetFamilyTypefaces(string familyName, [NotNullWhen(true)] out IReadOnlyList? familyTypefaces) 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/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/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/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.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 96f489a222..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,17 +46,17 @@ internal class BclLauncher : ILauncher ShellExecRaw($"xdg-open \\\"{args}\\\"", waitForExit: false); return true; } - else if (OperatingSystemEx.IsWindows() || OperatingSystemEx.IsMacOS()) + else if (OperatingSystem.IsWindows() || OperatingSystem.IsMacOS()) { var info = new ProcessStartInfo { - FileName = OperatingSystemEx.IsWindows() ? urlOrFile : "open", + FileName = OperatingSystem.IsWindows() ? urlOrFile : "open", CreateNoWindow = true, - UseShellExecute = OperatingSystemEx.IsWindows() + UseShellExecute = OperatingSystem.IsWindows() }; // Using the argument list avoids having to escape spaces and other special // characters that are part of valid macos file and folder paths. - if (OperatingSystemEx.IsMacOS()) + if (OperatingSystem.IsMacOS()) info.ArgumentList.Add(urlOrFile); using var process = Process.Start(info); return true; 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/Rendering/Composition/Compositor.cs b/src/Avalonia.Base/Rendering/Composition/Compositor.cs index 2398468456..2acda6c57d 100644 --- a/src/Avalonia.Base/Rendering/Composition/Compositor.cs +++ b/src/Avalonia.Base/Rendering/Composition/Compositor.cs @@ -51,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/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 f8382547b9..81a3c09b35 100644 --- a/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionTarget.cs +++ b/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionTarget.cs @@ -39,6 +39,11 @@ namespace Avalonia.Rendering.Composition.Server public ICompositionTargetDebugEvents? DebugEvents { get; set; } public int RenderedVisuals { get; set; } public int VisitedVisuals { get; set; } + + /// + /// 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) : base(compositor) @@ -125,6 +130,8 @@ namespace Avalonia.Rendering.Composition.Server public void Render() { + IsWaitingForReadyRenderTarget = false; + if (_disposed) return; @@ -143,11 +150,15 @@ namespace Avalonia.Rendering.Composition.Server try { if (_renderTarget == null && !_compositor.IsReadyToCreateRenderTarget(_surfaces())) + { + IsWaitingForReadyRenderTarget = IsEnabled; return; + } _renderTarget ??= _compositor.CreateRenderTarget(_surfaces()); } catch (RenderTargetNotReadyException) { + IsWaitingForReadyRenderTarget = IsEnabled; return; } catch (RenderTargetCorruptedException) @@ -164,7 +175,10 @@ namespace Avalonia.Rendering.Composition.Server return; if (!_renderTarget.IsReady) + { + IsWaitingForReadyRenderTarget = IsEnabled; return; + } var needLayer = _overlays.RequireLayer // Check if we don't need overlays // Check if render target can be rendered to directly and preserves the previous frame 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.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/ServerCompositor.cs b/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositor.cs index 76e649407f..b8cc5afca2 100644 --- a/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositor.cs +++ b/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositor.cs @@ -44,6 +44,9 @@ namespace Avalonia.Rendering.Composition.Server public CompositionOptions Options { get; } public ServerCompositorAnimations Animations { get; } public ReadbackIndices Readback { get; } = new(); + + private int _ticksSinceLastCommit; + private const int CommitGraceTicks = 10; public ServerCompositor(IRenderLoop renderLoop, IPlatformGraphics? platformGraphics, CompositionOptions options, @@ -64,6 +67,7 @@ namespace Avalonia.Rendering.Composition.Server { lock (_batches) _batches.Enqueue(batch); + _renderLoop.Wakeup(); } internal void UpdateServerTime() => ServerNow = Clock.Elapsed; @@ -72,6 +76,7 @@ namespace Avalonia.Rendering.Composition.Server readonly List _reusableToNotifyRenderedList = new(); void ApplyPendingBatches() { + bool hadBatches = false; while (true) { CompositionBatch batch; @@ -119,7 +124,13 @@ namespace Avalonia.Rendering.Composition.Server _reusableToNotifyProcessedList.Add(batch); LastBatchId = batch.SequenceId; + hadBatches = true; } + + if (hadBatches) + _ticksSinceLastCommit = 0; + else if (_ticksSinceLastCommit < int.MaxValue) + _ticksSinceLastCommit++; } void ReadServerJobs(BatchStreamReader reader, Queue queue, object endMarker) @@ -171,8 +182,10 @@ namespace Avalonia.Rendering.Composition.Server _reusableToNotifyRenderedList.Clear(); } - public void Render() => Render(true); - public void Render(bool catchExceptions) + bool IRenderLoopTask.Render() => ExecuteRender(true); + public void Render(bool catchExceptions) => ExecuteRender(catchExceptions); + + private bool ExecuteRender(bool catchExceptions) { if (Dispatcher.UIThread.CheckAccess()) { @@ -182,7 +195,7 @@ namespace Avalonia.Rendering.Composition.Server try { using (Dispatcher.UIThread.DisableProcessing()) - RenderReentrancySafe(catchExceptions); + return RenderReentrancySafe(catchExceptions); } finally { @@ -190,10 +203,10 @@ namespace Avalonia.Rendering.Composition.Server } } else - RenderReentrancySafe(catchExceptions); + return RenderReentrancySafe(catchExceptions); } - private void RenderReentrancySafe(bool catchExceptions) + private bool RenderReentrancySafe(bool catchExceptions) { lock (_lock) { @@ -202,7 +215,7 @@ namespace Avalonia.Rendering.Composition.Server try { _safeThread = Thread.CurrentThread; - RenderCore(catchExceptions); + return RenderCore(catchExceptions); } finally { @@ -235,17 +248,16 @@ namespace Avalonia.Rendering.Composition.Server return Stopwatch.GetElapsedTime(compositorGlobalPassesStarted); } - private void RenderCore(bool catchExceptions) + private bool RenderCore(bool catchExceptions) { - UpdateServerTime(); var compositorGlobalPassesElapsed = ExecuteGlobalPasses(); try { - if(!RenderInterface.IsReady) - return; + if (!RenderInterface.IsReady) + return true; RenderInterface.EnsureValidBackendContext(); ExecuteServerJobs(_receivedJobQueue); @@ -263,6 +275,18 @@ namespace Avalonia.Rendering.Composition.Server { Logger.TryGet(LogEventLevel.Error, LogArea.Visual)?.Log(this, "Exception when rendering: {Error}", e); } + + // Request a tick if we have active animations or if there are recent batches + if (Animations.NeedNextTick || _ticksSinceLastCommit < CommitGraceTicks) + return true; + + // Request a tick if we had unready targets in the last tick, to check if they are ready next time + foreach (var target in _activeTargets) + if (target.IsWaitingForReadyRenderTarget) + return true; + + // Otherwise there is no need to waste CPU cycles, tell the timer to pause + return false; } public void AddCompositionTarget(ServerCompositionTarget target) 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 7e1c9e711f..d9003659a1 100644 --- a/src/Avalonia.Base/Rendering/Composition/Transport/BatchStreamArrayPool.cs +++ b/src/Avalonia.Base/Rendering/Composition/Transport/BatchStreamArrayPool.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; using System.Linq; using System.Runtime.InteropServices; using System.Threading; @@ -13,52 +14,75 @@ namespace Avalonia.Rendering.Composition.Transport; /// 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); + GC.SuppressFinalize(this); - var updateRef = new WeakReference>(this); - if ( - reclaimImmediately - || Dispatcher.FromThread(Thread.CurrentThread) == null) - _reclaimImmediately = true; - else - StartUpdateTimer(startTimer, updateRef); + _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); @@ -67,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) @@ -90,6 +122,8 @@ internal abstract class BatchStreamPoolBase : IDisposable if (_usageStatistics[_usageStatisticsSlot] < _usage) _usageStatistics[_usageStatisticsSlot] = _usage; + OnActivity(); + if (_pool.Count != 0) return _pool.Pop(); } @@ -103,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/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/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/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/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/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/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/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/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/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 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. diff --git a/src/Avalonia.Base/composition-schema.xml b/src/Avalonia.Base/composition-schema.xml index ee12c2140d..f32bc97724 100644 --- a/src/Avalonia.Base/composition-schema.xml +++ b/src/Avalonia.Base/composition-schema.xml @@ -22,6 +22,7 @@ + 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/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/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/Button.cs b/src/Avalonia.Controls/Button.cs index 9e2d1725ce..ceadae432c 100644 --- a/src/Avalonia.Controls/Button.cs +++ b/src/Avalonia.Controls/Button.cs @@ -453,7 +453,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/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/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/ComboBox.cs b/src/Avalonia.Controls/ComboBox.cs index c4af8467c0..c977ca0a38 100644 --- a/src/Avalonia.Controls/ComboBox.cs +++ b/src/Avalonia.Controls/ComboBox.cs @@ -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/Control.cs b/src/Avalonia.Controls/Control.cs index 8efe5d6d66..edd647f772 100644 --- a/src/Avalonia.Controls/Control.cs +++ b/src/Avalonia.Controls/Control.cs @@ -380,7 +380,7 @@ namespace Avalonia.Controls } /// - protected override void OnGotFocus(GotFocusEventArgs e) + protected override void OnGotFocus(FocusChangedEventArgs e) { base.OnGotFocus(e); @@ -414,7 +414,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/GridSplitter.cs b/src/Avalonia.Controls/GridSplitter.cs index df502207c6..e44289649d 100644 --- a/src/Avalonia.Controls/GridSplitter.cs +++ b/src/Avalonia.Controls/GridSplitter.cs @@ -449,7 +449,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/ItemsControl.cs b/src/Avalonia.Controls/ItemsControl.cs index c176f3aeee..a3900874a1 100644 --- a/src/Avalonia.Controls/ItemsControl.cs +++ b/src/Avalonia.Controls/ItemsControl.cs @@ -532,7 +532,7 @@ namespace Avalonia.Controls _itemsPresenter = e.NameScope.Find("PART_ItemsPresenter"); } - protected override void OnGotFocus(GotFocusEventArgs e) + protected override void OnGotFocus(FocusChangedEventArgs e) { base.OnGotFocus(e); diff --git a/src/Avalonia.Controls/MaskedTextBox.cs b/src/Avalonia.Controls/MaskedTextBox.cs index 08855d253c..cfbd072a88 100644 --- a/src/Avalonia.Controls/MaskedTextBox.cs +++ b/src/Avalonia.Controls/MaskedTextBox.cs @@ -186,7 +186,7 @@ namespace Avalonia.Controls protected override Type StyleKeyOverride => typeof(TextBox); /// - protected override void OnGotFocus(GotFocusEventArgs e) + protected override void OnGotFocus(FocusChangedEventArgs e) { if (HidePromptOnLeave == true && MaskProvider != null) { @@ -283,7 +283,7 @@ namespace Avalonia.Controls } /// - protected override void OnLostFocus(RoutedEventArgs e) + protected override void OnLostFocus(FocusChangedEventArgs e) { if (HidePromptOnLeave && MaskProvider != null) { diff --git a/src/Avalonia.Controls/MenuItem.cs b/src/Avalonia.Controls/MenuItem.cs index 231dcc6a53..6bd0383463 100644 --- a/src/Avalonia.Controls/MenuItem.cs +++ b/src/Avalonia.Controls/MenuItem.cs @@ -506,7 +506,7 @@ namespace Avalonia.Controls } /// - protected override void OnGotFocus(GotFocusEventArgs e) + protected override void OnGotFocus(FocusChangedEventArgs e) { base.OnGotFocus(e); ItemsControlFromItemContainer(this)?.UpdateSelectionFromEvent(this, e); diff --git a/src/Avalonia.Controls/NativeControlHost.cs b/src/Avalonia.Controls/NativeControlHost.cs index 8328e0fd56..e36bec652c 100644 --- a/src/Avalonia.Controls/NativeControlHost.cs +++ b/src/Avalonia.Controls/NativeControlHost.cs @@ -4,6 +4,7 @@ using System.Diagnostics; using Avalonia.Automation.Peers; using Avalonia.Controls.Automation.Peers; using Avalonia.Controls.Platform; +using Avalonia.Layout; using Avalonia.Platform; using Avalonia.Rendering; using Avalonia.Threading; @@ -152,6 +153,18 @@ namespace Avalonia.Controls if (transformToVisual == null) return null; var transformedRect = new Rect(default, bounds.Size).TransformToAABB(transformToVisual.Value); + // Transformed rect should be pixel-rounded if layout rounding is enabled. + // This is important for native controls to align correctly with Avalonia's visual tree. + if (UseLayoutRounding) + { + var scale = LayoutHelper.GetLayoutScale(this); + var left = LayoutHelper.RoundLayoutValue(transformedRect.X, scale); + var top = LayoutHelper.RoundLayoutValue(transformedRect.Y, scale); + var right = LayoutHelper.RoundLayoutValue(transformedRect.Right, scale); + var bottom = LayoutHelper.RoundLayoutValue(transformedRect.Bottom, scale); + transformedRect = new Rect(new Point(left, top), new Point(right, bottom)); + } + return transformedRect; } diff --git a/src/Avalonia.Controls/NumericUpDown/NumericUpDown.cs b/src/Avalonia.Controls/NumericUpDown/NumericUpDown.cs index ab3861db47..ddaa57f2f2 100644 --- a/src/Avalonia.Controls/NumericUpDown/NumericUpDown.cs +++ b/src/Avalonia.Controls/NumericUpDown/NumericUpDown.cs @@ -440,14 +440,14 @@ namespace Avalonia.Controls } /// - protected override void OnGotFocus(GotFocusEventArgs e) + protected override void OnGotFocus(FocusChangedEventArgs e) { base.OnGotFocus(e); FocusChanged(IsKeyboardFocusWithin); } /// - protected override void OnLostFocus(RoutedEventArgs e) + protected override void OnLostFocus(FocusChangedEventArgs e) { CommitInput(true); base.OnLostFocus(e); diff --git a/src/Avalonia.Controls/Page/NavigationPage.cs b/src/Avalonia.Controls/Page/NavigationPage.cs index 2fb5235ff7..dd14d71a04 100644 --- a/src/Avalonia.Controls/Page/NavigationPage.cs +++ b/src/Avalonia.Controls/Page/NavigationPage.cs @@ -43,6 +43,7 @@ namespace Avalonia.Controls private ContentPresenter? _pagePresenter; private ContentPresenter? _pageBackPresenter; private CancellationTokenSource? _currentTransition; + private Task _lastPageTransitionTask = Task.CompletedTask; private CancellationTokenSource? _currentModalTransition; private Border? _navBar; private Border? _navBarShadow; @@ -769,15 +770,13 @@ namespace Avalonia.Controls page.SetInNavigationPage(true); UpdateActivePage(); - - previousPage?.SendNavigatedFrom(new NavigatedFromEventArgs(page, NavigationType.Push)); - page.SendNavigatedTo(new NavigatedToEventArgs(previousPage, NavigationType.Push)); - Pushed?.Invoke(this, new NavigationEventArgs(page, NavigationType.Push)); } /// - /// Performs the stack mutation and lifecycle events for a pop. The visual transition runs - /// subsequently via . + /// Performs the stack mutation for a pop. The visual transition runs + /// subsequently via . Callers are responsible + /// for firing lifecycle events via + /// after awaiting the page transition where possible. /// private Page? ExecutePopCore() { @@ -810,11 +809,6 @@ namespace Avalonia.Controls { old.Navigation = null; old.SetInNavigationPage(false); - - var newCurrentPage = CurrentPage; - old.SendNavigatedFrom(new NavigatedFromEventArgs(newCurrentPage, NavigationType.Pop)); - newCurrentPage?.SendNavigatedTo(new NavigatedToEventArgs(old, NavigationType.Pop)); - Popped?.Invoke(this, new NavigationEventArgs(old, NavigationType.Pop)); } return old; @@ -844,6 +838,12 @@ namespace Avalonia.Controls } ExecutePushCore(page, previousPage); + + await AwaitPageTransitionAsync(); + + previousPage?.SendNavigatedFrom(new NavigatedFromEventArgs(page, NavigationType.Push)); + page.SendNavigatedTo(new NavigatedToEventArgs(previousPage, NavigationType.Push)); + Pushed?.Invoke(this, new NavigationEventArgs(page, NavigationType.Push)); } finally { @@ -886,7 +886,14 @@ namespace Avalonia.Controls return null; } - return ExecutePopCore(); + var old = ExecutePopCore(); + + await AwaitPageTransitionAsync(); + + if (old != null) + SendPopLifecycleEvents(old, NavigationType.Pop); + + return old; } finally { @@ -931,6 +938,7 @@ namespace Avalonia.Controls } bool isIncc = Pages is INotifyCollectionChanged; + var poppedPages = new List(); void TearDownPopped(Page popped) { @@ -939,8 +947,7 @@ namespace Avalonia.Controls LogicalChildren.Remove(poppedLogical); popped.Navigation = null; popped.SetInNavigationPage(false); - popped.SendNavigatedFrom(new NavigatedFromEventArgs(rootPage, NavigationType.PopToRoot)); - Popped?.Invoke(this, new NavigationEventArgs(popped, NavigationType.PopToRoot)); + poppedPages.Add(popped); } if (Pages is Stack stack) @@ -962,6 +969,14 @@ namespace Avalonia.Controls _isPop = true; UpdateActivePage(); + await AwaitPageTransitionAsync(); + + foreach (var popped in poppedPages) + { + popped.SendNavigatedFrom(new NavigatedFromEventArgs(rootPage, NavigationType.PopToRoot)); + Popped?.Invoke(this, new NavigationEventArgs(popped, NavigationType.PopToRoot)); + } + var newCurrentPage = CurrentPage; if (newCurrentPage != null) @@ -1013,6 +1028,7 @@ namespace Avalonia.Controls } bool isIncc = Pages is INotifyCollectionChanged; + var poppedPages = new List(); void TearDownPopped(Page popped) { @@ -1021,8 +1037,7 @@ namespace Avalonia.Controls LogicalChildren.Remove(poppedLogical); popped.Navigation = null; popped.SetInNavigationPage(false); - popped.SendNavigatedFrom(new NavigatedFromEventArgs(page, NavigationType.Pop)); - Popped?.Invoke(this, new NavigationEventArgs(popped, NavigationType.Pop)); + poppedPages.Add(popped); } if (Pages is Stack stack) @@ -1044,6 +1059,14 @@ namespace Avalonia.Controls _isPop = true; UpdateActivePage(); + await AwaitPageTransitionAsync(); + + foreach (var popped in poppedPages) + { + popped.SendNavigatedFrom(new NavigatedFromEventArgs(page, NavigationType.Pop)); + Popped?.Invoke(this, new NavigationEventArgs(popped, NavigationType.Pop)); + } + var newCurrentPage = CurrentPage; if (newCurrentPage != null) { @@ -1356,7 +1379,9 @@ namespace Avalonia.Controls { if (stack.Count > 0 && ReferenceEquals(stack.Peek(), page)) { - ExecutePopCore(); + var old = ExecutePopCore(); + if (old != null) + SendPopLifecycleEvents(old, NavigationType.Pop); PageRemoved?.Invoke(this, new PageRemovedEventArgs(page)); return; } @@ -1387,7 +1412,9 @@ namespace Avalonia.Controls if (idx == list.Count - 1) { - ExecutePopCore(); + var old = ExecutePopCore(); + if (old != null) + SendPopLifecycleEvents(old, NavigationType.Pop); PageRemoved?.Invoke(this, new PageRemovedEventArgs(page)); return; } @@ -1595,12 +1622,14 @@ namespace Avalonia.Controls oldPresenter.ZIndex = 0; } - _ = RunPageTransitionAsync(resolvedTransition, oldPresenter, newPresenter, !isPop, cancel.Token); + _lastPageTransitionTask = RunPageTransitionAsync(resolvedTransition, oldPresenter, newPresenter, !isPop, cancel.Token); (_pagePresenter, _pageBackPresenter) = (newPresenter, oldPresenter); } else { + _lastPageTransitionTask = Task.CompletedTask; + _pagePresenter.Content = page; _pagePresenter.IsVisible = page != null; _pagePresenter.ZIndex = 0; @@ -1686,6 +1715,25 @@ namespace Avalonia.Controls from.Opacity = 1; } + private Task AwaitPageTransitionAsync() + { + var task = _lastPageTransitionTask; + _lastPageTransitionTask = Task.CompletedTask; + return task; + } + + /// + /// Fires lifecycle events after a pop: SendNavigatedFrom on the old page, + /// SendNavigatedTo on the new current page, and raises the Popped event. + /// + private void SendPopLifecycleEvents(Page oldPage, NavigationType navigationType) + { + var newCurrentPage = CurrentPage; + oldPage.SendNavigatedFrom(new NavigatedFromEventArgs(newCurrentPage, navigationType)); + newCurrentPage?.SendNavigatedTo(new NavigatedToEventArgs(oldPage, navigationType)); + Popped?.Invoke(this, new NavigationEventArgs(oldPage, navigationType)); + } + /// /// Swaps the top of the navigation stack with . /// diff --git a/src/Avalonia.Controls/PipsPager/PipsPager.cs b/src/Avalonia.Controls/PipsPager/PipsPager.cs new file mode 100644 index 0000000000..b976df4826 --- /dev/null +++ b/src/Avalonia.Controls/PipsPager/PipsPager.cs @@ -0,0 +1,662 @@ +using System; +using System.Threading; +using Avalonia.Threading; +using Avalonia.Controls.Metadata; +using Avalonia.Automation; +using Avalonia.Automation.Peers; +using Avalonia.Controls.Primitives; +using Avalonia.Data; +using Avalonia.Input; +using Avalonia.Interactivity; +using Avalonia.Layout; +using Avalonia.Styling; +using System.Collections.Generic; + +namespace Avalonia.Controls +{ + /// + /// Represents a control that lets the user navigate through a paginated collection using a set of pips. + /// + [TemplatePart(PART_PreviousButton, typeof(Button))] + [TemplatePart(PART_NextButton, typeof(Button))] + [TemplatePart(PART_PipsPagerList, typeof(ListBox))] + [PseudoClasses(PC_FirstPage, PC_LastPage, PC_Vertical, PC_Horizontal)] + public class PipsPager : TemplatedControl + { + private const string PART_PreviousButton = "PART_PreviousButton"; + private const string PART_NextButton = "PART_NextButton"; + private const string PART_PipsPagerList = "PART_PipsPagerList"; + + private const string PC_FirstPage = ":first-page"; + private const string PC_LastPage = ":last-page"; + private const string PC_Vertical = ":vertical"; + private const string PC_Horizontal = ":horizontal"; + + private Button? _previousButton; + private Button? _nextButton; + private ListBox? _pipsPagerList; + private bool _scrollPending; + private bool _updatingPagerSize; + private bool _isInitialLoad; + private int _lastSelectedPageIndex; + private CancellationTokenSource? _scrollAnimationCts; + private PipsPagerTemplateSettings _templateSettings = new PipsPagerTemplateSettings(); + + /// + /// Defines the property. + /// + public static readonly StyledProperty MaxVisiblePipsProperty = + AvaloniaProperty.Register(nameof(MaxVisiblePips), 5); + + /// + /// Defines the property. + /// + public static readonly StyledProperty IsNextButtonVisibleProperty = + AvaloniaProperty.Register(nameof(IsNextButtonVisible), true); + + /// + /// Defines the property. + /// + public static readonly StyledProperty NumberOfPagesProperty = + AvaloniaProperty.Register(nameof(NumberOfPages)); + + /// + /// Defines the property. + /// + public static readonly StyledProperty OrientationProperty = + AvaloniaProperty.Register(nameof(Orientation), Orientation.Horizontal); + + /// + /// Defines the property. + /// + public static readonly StyledProperty IsPreviousButtonVisibleProperty = + AvaloniaProperty.Register(nameof(IsPreviousButtonVisible), true); + + /// + /// Defines the property. + /// + public static readonly StyledProperty SelectedPageIndexProperty = + AvaloniaProperty.Register(nameof(SelectedPageIndex), + defaultBindingMode: BindingMode.TwoWay); + + /// + /// Defines the property. + /// + public static readonly DirectProperty TemplateSettingsProperty = + AvaloniaProperty.RegisterDirect(nameof(TemplateSettings), + x => x.TemplateSettings); + + /// + /// Defines the property. + /// + public static readonly StyledProperty PreviousButtonStyleProperty = + AvaloniaProperty.Register(nameof(PreviousButtonStyle)); + + /// + /// Defines the property. + /// + public static readonly StyledProperty NextButtonStyleProperty = + AvaloniaProperty.Register(nameof(NextButtonStyle)); + + /// + /// Defines the event. + /// + public static readonly RoutedEvent SelectedIndexChangedEvent = + RoutedEvent.Register(nameof(SelectedIndexChanged), RoutingStrategies.Bubble); + + /// + /// Occurs when the selected index has changed. + /// + public event EventHandler? SelectedIndexChanged + { + add => AddHandler(SelectedIndexChangedEvent, value); + remove => RemoveHandler(SelectedIndexChangedEvent, value); + } + + static PipsPager() + { + SelectedPageIndexProperty.Changed.AddClassHandler((x, e) => x.OnSelectedPageIndexChanged(e)); + NumberOfPagesProperty.Changed.AddClassHandler((x, e) => x.OnNumberOfPagesChanged(e)); + IsPreviousButtonVisibleProperty.Changed.AddClassHandler((x, e) => x.OnIsPreviousButtonVisibleChanged(e)); + IsNextButtonVisibleProperty.Changed.AddClassHandler((x, e) => x.OnIsNextButtonVisibleChanged(e)); + OrientationProperty.Changed.AddClassHandler((x, e) => x.OnOrientationChanged(e)); + MaxVisiblePipsProperty.Changed.AddClassHandler((x, e) => x.OnMaxVisiblePipsChanged(e)); + } + + /// + /// Initializes a new instance of . + /// + public PipsPager() + { + UpdatePseudoClasses(); + } + + /// + /// Gets or sets the maximum number of visible pips. + /// + public int MaxVisiblePips + { + get => GetValue(MaxVisiblePipsProperty); + set => SetValue(MaxVisiblePipsProperty, value); + } + + /// + /// Gets or sets the visibility of the next button. + /// + public bool IsNextButtonVisible + { + get => GetValue(IsNextButtonVisibleProperty); + set => SetValue(IsNextButtonVisibleProperty, value); + } + + /// + /// Gets or sets the number of pages. + /// + public int NumberOfPages + { + get => GetValue(NumberOfPagesProperty); + set => SetValue(NumberOfPagesProperty, value); + } + + /// + /// Gets or sets the orientation of the pips. + /// + public Orientation Orientation + { + get => GetValue(OrientationProperty); + set => SetValue(OrientationProperty, value); + } + + /// + /// Gets or sets the visibility of the previous button. + /// + public bool IsPreviousButtonVisible + { + get => GetValue(IsPreviousButtonVisibleProperty); + set => SetValue(IsPreviousButtonVisibleProperty, value); + } + + /// + /// Gets or sets the current selected page index. + /// + public int SelectedPageIndex + { + get => GetValue(SelectedPageIndexProperty); + set => SetValue(SelectedPageIndexProperty, value); + } + + /// + /// Gets the template settings. + /// + public PipsPagerTemplateSettings TemplateSettings + { + get => _templateSettings; + private set => SetAndRaise(TemplateSettingsProperty, ref _templateSettings, value); + } + + /// + /// Gets or sets the style for the previous button. + /// + public ControlTheme? PreviousButtonStyle + { + get => GetValue(PreviousButtonStyleProperty); + set => SetValue(PreviousButtonStyleProperty, value); + } + + /// + /// Gets or sets the style for the next button. + /// + public ControlTheme? NextButtonStyle + { + get => GetValue(NextButtonStyleProperty); + set => SetValue(NextButtonStyleProperty, value); + } + + /// + protected override AutomationPeer OnCreateAutomationPeer() + { + return new PipsPagerAutomationPeer(this); + } + + /// + protected override void OnApplyTemplate(TemplateAppliedEventArgs e) + { + base.OnApplyTemplate(e); + + _scrollAnimationCts?.Cancel(); + _scrollAnimationCts?.Dispose(); + _scrollAnimationCts = null; + _isInitialLoad = true; + + // Unsubscribe from previous button events + if (_previousButton != null) + { + _previousButton.Click -= PreviousButton_Click; + } + + if (_nextButton != null) + { + _nextButton.Click -= NextButton_Click; + } + + // Unsubscribe from previous list events + if (_pipsPagerList != null) + { + _pipsPagerList.SizeChanged -= OnPipsPagerListSizeChanged; + _pipsPagerList.ContainerPrepared -= OnContainerPrepared; + _pipsPagerList.ContainerIndexChanged -= OnContainerIndexChanged; + } + + // Get template parts + _previousButton = e.NameScope.Find + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/Avalonia.Themes.Simple/Controls/PipsPager.xaml b/src/Avalonia.Themes.Simple/Controls/PipsPager.xaml new file mode 100644 index 0000000000..388fc0e4d6 --- /dev/null +++ b/src/Avalonia.Themes.Simple/Controls/PipsPager.xaml @@ -0,0 +1,143 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/Avalonia.Themes.Simple/Controls/SimpleControls.xaml b/src/Avalonia.Themes.Simple/Controls/SimpleControls.xaml index 11a25dde9d..e5ba9163b5 100644 --- a/src/Avalonia.Themes.Simple/Controls/SimpleControls.xaml +++ b/src/Avalonia.Themes.Simple/Controls/SimpleControls.xaml @@ -40,6 +40,7 @@ + diff --git a/src/Avalonia.X11/X11Platform.cs b/src/Avalonia.X11/X11Platform.cs index bff986d2d1..566b0d907a 100644 --- a/src/Avalonia.X11/X11Platform.cs +++ b/src/Avalonia.X11/X11Platform.cs @@ -87,7 +87,7 @@ namespace Avalonia.X11 : new X11PlatformThreading(this); Dispatcher.InitializeUIThreadDispatcher(DispatcherImpl); AvaloniaLocator.CurrentMutable - .Bind().ToConstant(timer) + .Bind().ToConstant(RenderLoop.FromTimer(timer)) .Bind().ToConstant(new PlatformHotkeyConfiguration(KeyModifiers.Control)) .Bind().ToConstant(new KeyGestureFormatInfo(new Dictionary() { }, meta: "Super")) .Bind().ToFunc(() => KeyboardDevice) diff --git a/src/Avalonia.X11/X11Window.cs b/src/Avalonia.X11/X11Window.cs index 52456ca1b6..a57e1986ac 100644 --- a/src/Avalonia.X11/X11Window.cs +++ b/src/Avalonia.X11/X11Window.cs @@ -395,12 +395,7 @@ namespace Avalonia.X11 private static int GetProcessId() { -#if NET6_0_OR_GREATER var pid = Environment.ProcessId; -#else - using var currentProcess = Process.GetCurrentProcess(); - var pid = currentProcess.Id; -#endif return pid; } diff --git a/src/Browser/Avalonia.Browser/Rendering/BrowserRenderTimer.cs b/src/Browser/Avalonia.Browser/Rendering/BrowserRenderTimer.cs index fea94d0248..de9a167954 100644 --- a/src/Browser/Avalonia.Browser/Rendering/BrowserRenderTimer.cs +++ b/src/Browser/Avalonia.Browser/Rendering/BrowserRenderTimer.cs @@ -18,19 +18,16 @@ internal class BrowserRenderTimer : IRenderTimer public bool RunsInBackground { get; } - public event Action? Tick + public Action? Tick { - add + set { if (!BrowserWindowingPlatform.IsThreadingEnabled) StartOnThisThread(); - _tick += value; - } - remove - { - _tick -= value; + _tick = value; } + get => _tick; } public void StartOnThisThread() @@ -50,4 +47,4 @@ internal class BrowserRenderTimer : IRenderTimer tick.Invoke(TimeSpan.FromMilliseconds(timestamp)); } } -} +} \ No newline at end of file diff --git a/src/Browser/Avalonia.Browser/Rendering/BrowserSharedRenderLoop.cs b/src/Browser/Avalonia.Browser/Rendering/BrowserSharedRenderLoop.cs index 8d454ff582..1d9d1248b8 100644 --- a/src/Browser/Avalonia.Browser/Rendering/BrowserSharedRenderLoop.cs +++ b/src/Browser/Avalonia.Browser/Rendering/BrowserSharedRenderLoop.cs @@ -9,5 +9,5 @@ internal static class BrowserSharedRenderLoop { private static BrowserRenderTimer? s_browserUiRenderTimer; public static BrowserRenderTimer RenderTimer => s_browserUiRenderTimer ??= new BrowserRenderTimer(false); - public static Lazy RenderLoop = new(() => new RenderLoop(RenderTimer), true); + public static Lazy RenderLoop = new(() => Avalonia.Rendering.RenderLoop.FromTimer(RenderTimer), true); } diff --git a/src/Headless/Avalonia.Headless/AvaloniaHeadlessPlatform.cs b/src/Headless/Avalonia.Headless/AvaloniaHeadlessPlatform.cs index b56e686d4b..8e44942d32 100644 --- a/src/Headless/Avalonia.Headless/AvaloniaHeadlessPlatform.cs +++ b/src/Headless/Avalonia.Headless/AvaloniaHeadlessPlatform.cs @@ -15,6 +15,7 @@ namespace Avalonia.Headless public static class AvaloniaHeadlessPlatform { internal static Compositor? Compositor { get; private set; } + private static RenderTimer? s_renderTimer; private class RenderTimer : DefaultRenderTimer { @@ -85,7 +86,7 @@ namespace Avalonia.Headless .Bind().ToSingleton() .Bind().ToSingleton() .Bind().ToConstant(new KeyboardDevice()) - .Bind().ToConstant(new RenderTimer(60)) + .Bind().ToConstant(Rendering.RenderLoop.FromTimer(s_renderTimer = new RenderTimer(60))) .Bind().ToConstant(new HeadlessWindowingPlatform(opts.FrameBufferFormat)) .Bind().ToSingleton() .Bind().ToConstant(new KeyGestureFormatInfo(new Dictionary() { })); @@ -99,9 +100,8 @@ namespace Avalonia.Headless /// Count of frames to be ticked on the timer. public static void ForceRenderTimerTick(int count = 1) { - var timer = AvaloniaLocator.Current.GetService() as RenderTimer; for (var c = 0; c < count; c++) - timer?.ForceTick(); + s_renderTimer?.ForceTick(); } } diff --git a/src/Linux/Avalonia.LinuxFramebuffer/Input/EvDev/EvDevDevice.cs b/src/Linux/Avalonia.LinuxFramebuffer/Input/EvDev/EvDevDevice.cs index 92d8bbf268..85f190a848 100644 --- a/src/Linux/Avalonia.LinuxFramebuffer/Input/EvDev/EvDevDevice.cs +++ b/src/Linux/Avalonia.LinuxFramebuffer/Input/EvDev/EvDevDevice.cs @@ -18,11 +18,7 @@ namespace Avalonia.LinuxFramebuffer.Input.EvDev Fd = fd; _dev = dev; Name = Marshal.PtrToStringAnsi(NativeUnsafeMethods.libevdev_get_name(_dev)); -#if NET6_0_OR_GREATER foreach (EvType type in Enum.GetValues()) -#else - foreach (EvType type in Enum.GetValues(typeof(EvType))) -#endif { if (NativeUnsafeMethods.libevdev_has_event_type(dev, type) != 0) EventTypes.Add(type); diff --git a/src/Linux/Avalonia.LinuxFramebuffer/LinuxFramebufferPlatform.cs b/src/Linux/Avalonia.LinuxFramebuffer/LinuxFramebufferPlatform.cs index ee8b85919e..3239957d73 100644 --- a/src/Linux/Avalonia.LinuxFramebuffer/LinuxFramebufferPlatform.cs +++ b/src/Linux/Avalonia.LinuxFramebuffer/LinuxFramebufferPlatform.cs @@ -64,7 +64,7 @@ namespace Avalonia.LinuxFramebuffer Dispatcher.InitializeUIThreadDispatcher(new EpollDispatcherImpl(new ManualRawEventGrouperDispatchQueueDispatcherInputProvider(EventGrouperDispatchQueue))); AvaloniaLocator.CurrentMutable - .Bind().ToConstant(timer) + .Bind().ToConstant(RenderLoop.FromTimer(timer)) .Bind().ToTransient() .Bind().ToConstant(new KeyboardDevice()) .Bind().ToSingleton() diff --git a/src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/OnPlatformExtension.cs b/src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/OnPlatformExtension.cs index e393fbc7cf..0b1ae4638f 100644 --- a/src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/OnPlatformExtension.cs +++ b/src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/OnPlatformExtension.cs @@ -1,4 +1,4 @@ -using Avalonia.Compatibility; +using System; using Avalonia.Metadata; namespace Avalonia.Markup.Xaml.MarkupExtensions; @@ -76,13 +76,13 @@ public abstract class OnPlatformExtensionBase : IAddChild // IsOSPlatform might work better with trimming in the future, so it should be re-visited after .NET 8/9. return option switch { - "WINDOWS" => OperatingSystemEx.IsWindows(), - "OSX" => OperatingSystemEx.IsMacOS(), - "LINUX" => OperatingSystemEx.IsLinux(), - "ANDROID" => OperatingSystemEx.IsAndroid(), - "IOS" => OperatingSystemEx.IsIOS(), - "BROWSER" => OperatingSystemEx.IsBrowser(), - _ => OperatingSystemEx.IsOSPlatform(option) + "WINDOWS" => OperatingSystem.IsWindows(), + "OSX" => OperatingSystem.IsMacOS(), + "LINUX" => OperatingSystem.IsLinux(), + "ANDROID" => OperatingSystem.IsAndroid(), + "IOS" => OperatingSystem.IsIOS(), + "BROWSER" => OperatingSystem.IsBrowser(), + _ => OperatingSystem.IsOSPlatform(option) }; } } diff --git a/src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/ReflectionBindingExtension.cs b/src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/ReflectionBindingExtension.cs index f4b7864185..71d0902c0d 100644 --- a/src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/ReflectionBindingExtension.cs +++ b/src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/ReflectionBindingExtension.cs @@ -6,9 +6,7 @@ using Avalonia.Data; namespace Avalonia.Markup.Xaml.MarkupExtensions { [RequiresUnreferencedCode(TrimmingMessages.ReflectionBindingRequiresUnreferencedCodeMessage)] -#if NET8_0_OR_GREATER [RequiresDynamicCode(TrimmingMessages.ReflectionBindingRequiresDynamicCodeMessage)] -#endif public sealed class ReflectionBindingExtension : ReflectionBinding { /// diff --git a/src/Markup/Avalonia.Markup.Xaml/XamlIl/Runtime/XamlIlRuntimeHelpers.cs b/src/Markup/Avalonia.Markup.Xaml/XamlIl/Runtime/XamlIlRuntimeHelpers.cs index f98a1cc60b..d2f8fe437e 100644 --- a/src/Markup/Avalonia.Markup.Xaml/XamlIl/Runtime/XamlIlRuntimeHelpers.cs +++ b/src/Markup/Avalonia.Markup.Xaml/XamlIl/Runtime/XamlIlRuntimeHelpers.cs @@ -129,22 +129,11 @@ namespace Avalonia.Markup.Xaml.XamlIl.Runtime return false; } -#if NET6_0_OR_GREATER if (!CollectionsMarshal.AsSpan(resourceNodes).SequenceEqual(lastResourceNodes)) { cachedResourceNodes = null; return false; } -#else - for (var i = 0; i < lastResourceNodes.Length; ++i) - { - if (lastResourceNodes[i] != resourceNodes[i]) - { - cachedResourceNodes = null; - return false; - } - } -#endif cachedResourceNodes = lastResourceNodes; return true; diff --git a/src/Markup/Avalonia.Markup/Data/Binding.cs b/src/Markup/Avalonia.Markup/Data/Binding.cs index 4e624b6479..931c17fd0e 100644 --- a/src/Markup/Avalonia.Markup/Data/Binding.cs +++ b/src/Markup/Avalonia.Markup/Data/Binding.cs @@ -8,9 +8,7 @@ namespace Avalonia.Data; /// for new code. /// [RequiresUnreferencedCode(TrimmingMessages.ReflectionBindingRequiresUnreferencedCodeMessage)] -#if NET8_0_OR_GREATER - [RequiresDynamicCode(TrimmingMessages.ReflectionBindingRequiresDynamicCodeMessage)] -#endif +[RequiresDynamicCode(TrimmingMessages.ReflectionBindingRequiresDynamicCodeMessage)] public class Binding : ReflectionBinding { public Binding() { } diff --git a/src/Shared/ModuleInitializer.cs b/src/Shared/ModuleInitializer.cs index e58b296474..d65fd7ffa4 100644 --- a/src/Shared/ModuleInitializer.cs +++ b/src/Shared/ModuleInitializer.cs @@ -2,7 +2,7 @@ namespace System.Runtime.CompilerServices { #if NETSTANDARD2_0 [AttributeUsage(AttributeTargets.Method)] - internal sealed class ModuleInitializerAttribute : Attribute + internal sealed class ModuleInitializerhAttribute : Attribute { } diff --git a/src/Skia/Avalonia.Skia/Gpu/Metal/SkiaMetalGpu.cs b/src/Skia/Avalonia.Skia/Gpu/Metal/SkiaMetalGpu.cs index ef3c191904..640b415d6f 100644 --- a/src/Skia/Avalonia.Skia/Gpu/Metal/SkiaMetalGpu.cs +++ b/src/Skia/Avalonia.Skia/Gpu/Metal/SkiaMetalGpu.cs @@ -105,7 +105,7 @@ internal class SkiaMetalGpu : ISkiaGpu session.IsYFlipped ? GRSurfaceOrigin.BottomLeft : GRSurfaceOrigin.TopLeft, SKColorType.Bgra8888); - return new SkiaMetalRenderSession(_gpu, surface, session); + return new SkiaMetalRenderSession(_gpu, surface, session, backendTarget); } public bool IsCorrupted => false; @@ -118,14 +118,17 @@ internal class SkiaMetalGpu : ISkiaGpu private readonly SkiaMetalGpu _gpu; private SKSurface? _surface; private IMetalPlatformSurfaceRenderingSession? _session; + private GRBackendRenderTarget? _backendTarget; - public SkiaMetalRenderSession(SkiaMetalGpu gpu, + public SkiaMetalRenderSession(SkiaMetalGpu gpu, SKSurface surface, - IMetalPlatformSurfaceRenderingSession session) + IMetalPlatformSurfaceRenderingSession session, + GRBackendRenderTarget backendTarget) { _gpu = gpu; _surface = surface; _session = session; + _backendTarget = backendTarget; } public void Dispose() @@ -133,11 +136,13 @@ internal class SkiaMetalGpu : ISkiaGpu _surface?.Canvas.Flush(); _surface?.Flush(); _gpu._context?.Flush(); - + _surface?.Dispose(); _surface = null; _session?.Dispose(); _session = null; + _backendTarget?.Dispose(); + _backendTarget = null; } public GRContext GrContext => _gpu._context!; diff --git a/src/Skia/Avalonia.Skia/Helpers/PixelFormatHelper.cs b/src/Skia/Avalonia.Skia/Helpers/PixelFormatHelper.cs index f1cd39b2a7..21c9f3cdf9 100644 --- a/src/Skia/Avalonia.Skia/Helpers/PixelFormatHelper.cs +++ b/src/Skia/Avalonia.Skia/Helpers/PixelFormatHelper.cs @@ -1,5 +1,4 @@ -using Avalonia.Compatibility; -using Avalonia.Platform; +using Avalonia.Platform; using SkiaSharp; namespace Avalonia.Skia.Helpers diff --git a/src/Skia/Avalonia.Skia/SKRoundRectCache.cs b/src/Skia/Avalonia.Skia/SKRoundRectCache.cs index b84c61303d..7b041fb9f1 100644 --- a/src/Skia/Avalonia.Skia/SKRoundRectCache.cs +++ b/src/Skia/Avalonia.Skia/SKRoundRectCache.cs @@ -91,13 +91,7 @@ namespace Avalonia.Skia base.Clear(); // Clear out the cache of SKPoint arrays. -#if NET6_0_OR_GREATER _radiiCache.Clear(); -#else - while (_radiiCache.TryTake(out var item)) - { - } -#endif } } } diff --git a/src/Windows/Avalonia.Win32.Automation/AutomationNode.cs b/src/Windows/Avalonia.Win32.Automation/AutomationNode.cs index bcf3e782b4..2ae4294dcb 100644 --- a/src/Windows/Avalonia.Win32.Automation/AutomationNode.cs +++ b/src/Windows/Avalonia.Win32.Automation/AutomationNode.cs @@ -18,15 +18,8 @@ using UIA = Avalonia.Win32.Automation.Interop; namespace Avalonia.Win32.Automation { -#if NET8_0_OR_GREATER [GeneratedComClass] internal partial class AutomationNode : -#else -#if NET6_0_OR_GREATER - [RequiresUnreferencedCode("Requires .NET COM interop")] -#endif - internal partial class AutomationNode : MarshalByRefObject, -#endif IRawElementProviderSimple, IRawElementProviderSimple2, IRawElementProviderFragment, @@ -202,9 +195,7 @@ namespace Avalonia.Win32.Automation public void SetFocus() => InvokeSync(() => Peer.SetFocus()); -#if NET6_0_OR_GREATER [return: NotNullIfNotNull(nameof(peer))] -#endif public static AutomationNode? GetOrCreate(AutomationPeer? peer) { return peer is null ? null : s_nodes.GetValue(peer, Create); @@ -434,12 +425,7 @@ namespace Avalonia.Win32.Automation private static int GetProcessId() { -#if NET6_0_OR_GREATER return Environment.ProcessId; -#else - using var proccess = Process.GetCurrentProcess(); - return proccess.Id; -#endif } } } diff --git a/src/Windows/Avalonia.Win32.Automation/Interop/IDockProvider.cs b/src/Windows/Avalonia.Win32.Automation/Interop/IDockProvider.cs index c65e76366a..80509e8aa7 100644 --- a/src/Windows/Avalonia.Win32.Automation/Interop/IDockProvider.cs +++ b/src/Windows/Avalonia.Win32.Automation/Interop/IDockProvider.cs @@ -14,12 +14,7 @@ internal enum DockPosition None } -#if NET8_0_OR_GREATER [GeneratedComInterface(Options = ComInterfaceOptions.ManagedObjectWrapper)] -#else -[ComImport()] -[InterfaceType(ComInterfaceType.InterfaceIsIUnknown)] -#endif [Guid("159bc72c-4ad3-485e-9637-d7052edf0146")] internal partial interface IDockProvider { diff --git a/src/Windows/Avalonia.Win32.Automation/Interop/IExpandCollapseProvider.cs b/src/Windows/Avalonia.Win32.Automation/Interop/IExpandCollapseProvider.cs index ee04a24ce7..7e5cc048de 100644 --- a/src/Windows/Avalonia.Win32.Automation/Interop/IExpandCollapseProvider.cs +++ b/src/Windows/Avalonia.Win32.Automation/Interop/IExpandCollapseProvider.cs @@ -4,12 +4,7 @@ using Avalonia.Automation; namespace Avalonia.Win32.Automation.Interop; -#if NET8_0_OR_GREATER [GeneratedComInterface(Options = ComInterfaceOptions.ManagedObjectWrapper)] -#else -[ComImport()] -[InterfaceType(ComInterfaceType.InterfaceIsIUnknown)] -#endif [Guid("d847d3a5-cab0-4a98-8c32-ecb45c59ad24")] internal partial interface IExpandCollapseProvider { diff --git a/src/Windows/Avalonia.Win32.Automation/Interop/IGridItemProvider.cs b/src/Windows/Avalonia.Win32.Automation/Interop/IGridItemProvider.cs index f764427417..541f656a34 100644 --- a/src/Windows/Avalonia.Win32.Automation/Interop/IGridItemProvider.cs +++ b/src/Windows/Avalonia.Win32.Automation/Interop/IGridItemProvider.cs @@ -3,12 +3,7 @@ using System.Runtime.InteropServices.Marshalling; namespace Avalonia.Win32.Automation.Interop; -#if NET8_0_OR_GREATER [GeneratedComInterface(Options = ComInterfaceOptions.ManagedObjectWrapper)] -#else -[ComImport()] -[InterfaceType(ComInterfaceType.InterfaceIsIUnknown)] -#endif [Guid("d02541f1-fb81-4d64-ae32-f520f8a6dbd1")] internal partial interface IGridItemProvider { diff --git a/src/Windows/Avalonia.Win32.Automation/Interop/IGridProvider.cs b/src/Windows/Avalonia.Win32.Automation/Interop/IGridProvider.cs index cfc295fa7d..b3a7cfd47a 100644 --- a/src/Windows/Avalonia.Win32.Automation/Interop/IGridProvider.cs +++ b/src/Windows/Avalonia.Win32.Automation/Interop/IGridProvider.cs @@ -3,12 +3,7 @@ using System.Runtime.InteropServices.Marshalling; namespace Avalonia.Win32.Automation.Interop; -#if NET8_0_OR_GREATER [GeneratedComInterface(Options = ComInterfaceOptions.ManagedObjectWrapper)] -#else -[ComImport()] -[InterfaceType(ComInterfaceType.InterfaceIsIUnknown)] -#endif [Guid("b17d6187-0907-464b-a168-0ef17a1572b1")] internal partial interface IGridProvider { diff --git a/src/Windows/Avalonia.Win32.Automation/Interop/IInvokeProvider.cs b/src/Windows/Avalonia.Win32.Automation/Interop/IInvokeProvider.cs index 7737a1bb74..1ef0cac481 100644 --- a/src/Windows/Avalonia.Win32.Automation/Interop/IInvokeProvider.cs +++ b/src/Windows/Avalonia.Win32.Automation/Interop/IInvokeProvider.cs @@ -7,12 +7,7 @@ using System.Runtime.InteropServices.Marshalling; namespace Avalonia.Win32.Automation.Interop; -#if NET8_0_OR_GREATER [GeneratedComInterface(Options = ComInterfaceOptions.ManagedObjectWrapper)] -#else -[ComImport()] -[InterfaceType(ComInterfaceType.InterfaceIsIUnknown)] -#endif [Guid("54fcb24b-e18e-47a2-b4d3-eccbe77599a2")] internal partial interface IInvokeProvider { diff --git a/src/Windows/Avalonia.Win32.Automation/Interop/IMultipleViewProvider.cs b/src/Windows/Avalonia.Win32.Automation/Interop/IMultipleViewProvider.cs index dcd0d35e74..c21790acab 100644 --- a/src/Windows/Avalonia.Win32.Automation/Interop/IMultipleViewProvider.cs +++ b/src/Windows/Avalonia.Win32.Automation/Interop/IMultipleViewProvider.cs @@ -5,12 +5,8 @@ using Avalonia.Win32.Automation.Marshalling; namespace Avalonia.Win32.Automation.Interop; -#if NET8_0_OR_GREATER [GeneratedComInterface(Options = ComInterfaceOptions.ManagedObjectWrapper)] -#else -[ComImport()] -[InterfaceType(ComInterfaceType.InterfaceIsIUnknown)] -#endif + [Guid("6278cab1-b556-4a1a-b4e0-418acc523201")] internal partial interface IMultipleViewProvider { @@ -18,8 +14,6 @@ internal partial interface IMultipleViewProvider string GetViewName(int viewId); void SetCurrentView(int viewId); int GetCurrentView(); -#if NET8_0_OR_GREATER [return: MarshalUsing(typeof(SafeArrayMarshaller))] -#endif int[] GetSupportedViews(); } diff --git a/src/Windows/Avalonia.Win32.Automation/Interop/IRangeValueProvider.cs b/src/Windows/Avalonia.Win32.Automation/Interop/IRangeValueProvider.cs index a8f921fa26..089b1d65ad 100644 --- a/src/Windows/Avalonia.Win32.Automation/Interop/IRangeValueProvider.cs +++ b/src/Windows/Avalonia.Win32.Automation/Interop/IRangeValueProvider.cs @@ -3,12 +3,7 @@ using System.Runtime.InteropServices.Marshalling; namespace Avalonia.Win32.Automation.Interop; -#if NET8_0_OR_GREATER [GeneratedComInterface(Options = ComInterfaceOptions.ManagedObjectWrapper)] -#else -[ComImport()] -[InterfaceType(ComInterfaceType.InterfaceIsIUnknown)] -#endif [Guid("36dc7aef-33e6-4691-afe1-2be7274b3d33")] internal partial interface IRangeValueProvider { diff --git a/src/Windows/Avalonia.Win32.Automation/Interop/IRawElementProviderAdviseEvents.cs b/src/Windows/Avalonia.Win32.Automation/Interop/IRawElementProviderAdviseEvents.cs index 9d2e16ab94..6f73b73790 100644 --- a/src/Windows/Avalonia.Win32.Automation/Interop/IRawElementProviderAdviseEvents.cs +++ b/src/Windows/Avalonia.Win32.Automation/Interop/IRawElementProviderAdviseEvents.cs @@ -5,24 +5,15 @@ using Avalonia.Win32.Automation.Marshalling; namespace Avalonia.Win32.Automation.Interop; -#if NET8_0_OR_GREATER [GeneratedComInterface(Options = ComInterfaceOptions.ManagedObjectWrapper)] -#else -[ComImport()] -[InterfaceType(ComInterfaceType.InterfaceIsIUnknown)] -#endif [Guid("a407b27b-0f6d-4427-9292-473c7bf93258")] internal partial interface IRawElementProviderAdviseEvents { void AdviseEventAdded(int eventId, -#if NET8_0_OR_GREATER [MarshalUsing(typeof(SafeArrayMarshaller))] -#endif int[] properties); void AdviseEventRemoved(int eventId, -#if NET8_0_OR_GREATER [MarshalUsing(typeof(SafeArrayMarshaller))] -#endif int[] properties); } diff --git a/src/Windows/Avalonia.Win32.Automation/Interop/IRawElementProviderFragment.cs b/src/Windows/Avalonia.Win32.Automation/Interop/IRawElementProviderFragment.cs index 0bb56c8b68..7550ecf0df 100644 --- a/src/Windows/Avalonia.Win32.Automation/Interop/IRawElementProviderFragment.cs +++ b/src/Windows/Avalonia.Win32.Automation/Interop/IRawElementProviderFragment.cs @@ -15,24 +15,15 @@ internal enum NavigateDirection LastChild, } -#if NET8_0_OR_GREATER [GeneratedComInterface(Options = ComInterfaceOptions.ManagedObjectWrapper)] -#else -[ComImport()] -[InterfaceType(ComInterfaceType.InterfaceIsIUnknown)] -#endif [Guid("f7063da8-8359-439c-9297-bbc5299a7d87")] internal partial interface IRawElementProviderFragment { IRawElementProviderFragment? Navigate(NavigateDirection direction); -#if NET8_0_OR_GREATER [return: MarshalUsing(typeof(SafeArrayMarshaller))] -#endif int[]? GetRuntimeId(); Rect GetBoundingRectangle(); -#if NET8_0_OR_GREATER [return: MarshalUsing(typeof(SafeArrayMarshaller))] -#endif IRawElementProviderSimple[]? GetEmbeddedFragmentRoots(); void SetFocus(); IRawElementProviderFragmentRoot? GetFragmentRoot(); diff --git a/src/Windows/Avalonia.Win32.Automation/Interop/IRawElementProviderFragmentRoot.cs b/src/Windows/Avalonia.Win32.Automation/Interop/IRawElementProviderFragmentRoot.cs index 349e58b7b3..430559665a 100644 --- a/src/Windows/Avalonia.Win32.Automation/Interop/IRawElementProviderFragmentRoot.cs +++ b/src/Windows/Avalonia.Win32.Automation/Interop/IRawElementProviderFragmentRoot.cs @@ -3,12 +3,7 @@ using System.Runtime.InteropServices.Marshalling; namespace Avalonia.Win32.Automation.Interop; -#if NET8_0_OR_GREATER [GeneratedComInterface(Options = ComInterfaceOptions.ManagedObjectWrapper)] -#else -[ComImport()] -[InterfaceType(ComInterfaceType.InterfaceIsIUnknown)] -#endif [Guid("620ce2a5-ab8f-40a9-86cb-de3c75599b58")] internal partial interface IRawElementProviderFragmentRoot { diff --git a/src/Windows/Avalonia.Win32.Automation/Interop/IRawElementProviderSimple.cs b/src/Windows/Avalonia.Win32.Automation/Interop/IRawElementProviderSimple.cs index bf70aa1f40..6f2233d7f6 100644 --- a/src/Windows/Avalonia.Win32.Automation/Interop/IRawElementProviderSimple.cs +++ b/src/Windows/Avalonia.Win32.Automation/Interop/IRawElementProviderSimple.cs @@ -307,21 +307,14 @@ internal enum UiaLiveSetting Assertive, }; -#if NET8_0_OR_GREATER [GeneratedComInterface] -#else -[ComImport()] -[InterfaceType(ComInterfaceType.InterfaceIsIUnknown)] -#endif [Guid("d6dd68d1-86fd-4332-8666-9abedea2d24c")] internal partial interface IRawElementProviderSimple { ProviderOptions GetProviderOptions(); [return: MarshalAs(UnmanagedType.Interface)] object? GetPatternProvider(int patternId); -#if NET8_0_OR_GREATER [return: MarshalUsing(typeof(ComVariantMarshaller))] -#endif object? GetPropertyValue(int propertyId); IRawElementProviderSimple? GetHostRawElementProvider(); } diff --git a/src/Windows/Avalonia.Win32.Automation/Interop/IRawElementProviderSimple2.cs b/src/Windows/Avalonia.Win32.Automation/Interop/IRawElementProviderSimple2.cs index 7bd48f4e78..ed62c621d4 100644 --- a/src/Windows/Avalonia.Win32.Automation/Interop/IRawElementProviderSimple2.cs +++ b/src/Windows/Avalonia.Win32.Automation/Interop/IRawElementProviderSimple2.cs @@ -3,24 +3,9 @@ using System.Runtime.InteropServices.Marshalling; namespace Avalonia.Win32.Automation.Interop; -#if NET8_0_OR_GREATER [GeneratedComInterface(Options = ComInterfaceOptions.ManagedObjectWrapper)] -#else -[ComImport()] -[InterfaceType(ComInterfaceType.InterfaceIsIUnknown)] -#endif [Guid("a0a839a9-8da1-4a82-806a-8e0d44e79f56")] internal partial interface IRawElementProviderSimple2 : IRawElementProviderSimple { -#if !NET8_0_OR_GREATER - // Hack for the legacy COM interop - // See https://learn.microsoft.com/en-us/dotnet/standard/native-interop/comwrappers-source-generation#derived-interfaces - new ProviderOptions GetProviderOptions(); - [return: MarshalAs(UnmanagedType.Interface)] - new object? GetPatternProvider(int patternId); - [return: MarshalAs(UnmanagedType.Struct)] - new object? GetPropertyValue(int propertyId); - new IRawElementProviderSimple? GetHostRawElementProvider(); -#endif void ShowContextMenu(); } diff --git a/src/Windows/Avalonia.Win32.Automation/Interop/IScrollItemProvider.cs b/src/Windows/Avalonia.Win32.Automation/Interop/IScrollItemProvider.cs index 8e022c988d..20d5690de3 100644 --- a/src/Windows/Avalonia.Win32.Automation/Interop/IScrollItemProvider.cs +++ b/src/Windows/Avalonia.Win32.Automation/Interop/IScrollItemProvider.cs @@ -3,12 +3,7 @@ using System.Runtime.InteropServices.Marshalling; namespace Avalonia.Win32.Automation.Interop; -#if NET8_0_OR_GREATER [GeneratedComInterface(Options = ComInterfaceOptions.ManagedObjectWrapper)] -#else -[ComImport()] -[InterfaceType(ComInterfaceType.InterfaceIsIUnknown)] -#endif [Guid("2360c714-4bf1-4b26-ba65-9b21316127eb")] internal partial interface IScrollItemProvider { diff --git a/src/Windows/Avalonia.Win32.Automation/Interop/IScrollProvider.cs b/src/Windows/Avalonia.Win32.Automation/Interop/IScrollProvider.cs index 1113685592..bda05f540f 100644 --- a/src/Windows/Avalonia.Win32.Automation/Interop/IScrollProvider.cs +++ b/src/Windows/Avalonia.Win32.Automation/Interop/IScrollProvider.cs @@ -4,12 +4,7 @@ using Avalonia.Automation.Provider; namespace Avalonia.Win32.Automation.Interop; -#if NET8_0_OR_GREATER [GeneratedComInterface(Options = ComInterfaceOptions.ManagedObjectWrapper)] -#else -[ComImport()] -[InterfaceType(ComInterfaceType.InterfaceIsIUnknown)] -#endif [Guid("b38b8077-1fc3-42a5-8cae-d40c2215055a")] internal partial interface IScrollProvider { diff --git a/src/Windows/Avalonia.Win32.Automation/Interop/ISelectionItemProvider.cs b/src/Windows/Avalonia.Win32.Automation/Interop/ISelectionItemProvider.cs index a4f4d56e54..d36cb605a5 100644 --- a/src/Windows/Avalonia.Win32.Automation/Interop/ISelectionItemProvider.cs +++ b/src/Windows/Avalonia.Win32.Automation/Interop/ISelectionItemProvider.cs @@ -3,12 +3,7 @@ using System.Runtime.InteropServices.Marshalling; namespace Avalonia.Win32.Automation.Interop; -#if NET8_0_OR_GREATER [GeneratedComInterface(Options = ComInterfaceOptions.ManagedObjectWrapper)] -#else -[ComImport()] -[InterfaceType(ComInterfaceType.InterfaceIsIUnknown)] -#endif [Guid("2acad808-b2d4-452d-a407-91ff1ad167b2")] internal partial interface ISelectionItemProvider { diff --git a/src/Windows/Avalonia.Win32.Automation/Interop/ISelectionProvider.cs b/src/Windows/Avalonia.Win32.Automation/Interop/ISelectionProvider.cs index 2a30c97f18..3db1e3a4a4 100644 --- a/src/Windows/Avalonia.Win32.Automation/Interop/ISelectionProvider.cs +++ b/src/Windows/Avalonia.Win32.Automation/Interop/ISelectionProvider.cs @@ -5,18 +5,11 @@ using Avalonia.Win32.Automation.Marshalling; namespace Avalonia.Win32.Automation.Interop; -#if NET8_0_OR_GREATER [GeneratedComInterface(Options = ComInterfaceOptions.ManagedObjectWrapper)] -#else -[ComImport()] -[InterfaceType(ComInterfaceType.InterfaceIsIUnknown)] -#endif [Guid("fb8b03af-3bdf-48d4-bd36-1a65793be168")] internal partial interface ISelectionProvider { -#if NET8_0_OR_GREATER [return: MarshalUsing(typeof(SafeArrayMarshaller))] -#endif IRawElementProviderSimple[] GetSelection(); [return: MarshalAs(UnmanagedType.Bool)] diff --git a/src/Windows/Avalonia.Win32.Automation/Interop/ISynchronizedInputProvider.cs b/src/Windows/Avalonia.Win32.Automation/Interop/ISynchronizedInputProvider.cs index 75850461c3..24f8948f12 100644 --- a/src/Windows/Avalonia.Win32.Automation/Interop/ISynchronizedInputProvider.cs +++ b/src/Windows/Avalonia.Win32.Automation/Interop/ISynchronizedInputProvider.cs @@ -13,12 +13,8 @@ internal enum SynchronizedInputType MouseRightButtonUp = 0x10, MouseRightButtonDown = 0x20 } -#if NET8_0_OR_GREATER + [GeneratedComInterface(Options = ComInterfaceOptions.ManagedObjectWrapper)] -#else -[ComImport()] -[InterfaceType(ComInterfaceType.InterfaceIsIUnknown)] -#endif [Guid("29db1a06-02ce-4cf7-9b42-565d4fab20ee")] internal partial interface ISynchronizedInputProvider { diff --git a/src/Windows/Avalonia.Win32.Automation/Interop/ITableItemProvider.cs b/src/Windows/Avalonia.Win32.Automation/Interop/ITableItemProvider.cs index 75bdf48bb8..33b9d30062 100644 --- a/src/Windows/Avalonia.Win32.Automation/Interop/ITableItemProvider.cs +++ b/src/Windows/Avalonia.Win32.Automation/Interop/ITableItemProvider.cs @@ -5,21 +5,12 @@ using Avalonia.Win32.Automation.Marshalling; namespace Avalonia.Win32.Automation.Interop; -#if NET8_0_OR_GREATER [GeneratedComInterface(Options = ComInterfaceOptions.ManagedObjectWrapper)] -#else -[ComImport()] -[InterfaceType(ComInterfaceType.InterfaceIsIUnknown)] -#endif [Guid("b9734fa6-771f-4d78-9c90-2517999349cd")] internal partial interface ITableItemProvider { -#if NET8_0_OR_GREATER [return: MarshalUsing(typeof(SafeArrayMarshaller))] -#endif IRawElementProviderSimple[] GetRowHeaderItems(); -#if NET8_0_OR_GREATER [return: MarshalUsing(typeof(SafeArrayMarshaller))] -#endif IRawElementProviderSimple[] GetColumnHeaderItems(); } diff --git a/src/Windows/Avalonia.Win32.Automation/Interop/ITableProvider.cs b/src/Windows/Avalonia.Win32.Automation/Interop/ITableProvider.cs index 6acacbdf5d..5f4bc589d6 100644 --- a/src/Windows/Avalonia.Win32.Automation/Interop/ITableProvider.cs +++ b/src/Windows/Avalonia.Win32.Automation/Interop/ITableProvider.cs @@ -12,22 +12,14 @@ internal enum RowOrColumnMajor ColumnMajor, Indeterminate, } -#if NET8_0_OR_GREATER + [GeneratedComInterface(Options = ComInterfaceOptions.ManagedObjectWrapper)] -#else -[ComImport()] -[InterfaceType(ComInterfaceType.InterfaceIsIUnknown)] -#endif [Guid("9c860395-97b3-490a-b52a-858cc22af166")] internal partial interface ITableProvider { -#if NET8_0_OR_GREATER [return: MarshalUsing(typeof(SafeArrayMarshaller))] -#endif IRawElementProviderSimple[] GetRowHeaders(); -#if NET8_0_OR_GREATER [return: MarshalUsing(typeof(SafeArrayMarshaller))] -#endif IRawElementProviderSimple[] GetColumnHeaders(); RowOrColumnMajor GetRowOrColumnMajor(); } diff --git a/src/Windows/Avalonia.Win32.Automation/Interop/ITextProvider.cs b/src/Windows/Avalonia.Win32.Automation/Interop/ITextProvider.cs index 63a92ce547..d3c94ecd07 100644 --- a/src/Windows/Avalonia.Win32.Automation/Interop/ITextProvider.cs +++ b/src/Windows/Avalonia.Win32.Automation/Interop/ITextProvider.cs @@ -15,22 +15,13 @@ internal enum SupportedTextSelection Multiple, } -#if NET8_0_OR_GREATER [GeneratedComInterface(Options = ComInterfaceOptions.ManagedObjectWrapper)] -#else -[ComImport()] -[InterfaceType(ComInterfaceType.InterfaceIsIUnknown)] -#endif [Guid("3589c92c-63f3-4367-99bb-ada653b77cf2")] internal partial interface ITextProvider { -#if NET8_0_OR_GREATER [return: MarshalUsing(typeof(SafeArrayMarshaller))] -#endif ITextRangeProvider[] GetSelection(); -#if NET8_0_OR_GREATER [return: MarshalUsing(typeof(SafeArrayMarshaller))] -#endif ITextRangeProvider[] GetVisibleRanges(); ITextRangeProvider RangeFromChild(IRawElementProviderSimple childElement); diff --git a/src/Windows/Avalonia.Win32.Automation/Interop/ITextRangeProvider.cs b/src/Windows/Avalonia.Win32.Automation/Interop/ITextRangeProvider.cs index 18f167a87a..72f43ef8b2 100644 --- a/src/Windows/Avalonia.Win32.Automation/Interop/ITextRangeProvider.cs +++ b/src/Windows/Avalonia.Win32.Automation/Interop/ITextRangeProvider.cs @@ -22,12 +22,7 @@ internal enum TextUnit Document = 6, } -#if NET8_0_OR_GREATER [GeneratedComInterface(Options = ComInterfaceOptions.ManagedObjectWrapper)] -#else -[ComImport()] -[InterfaceType(ComInterfaceType.InterfaceIsIUnknown)] -#endif [Guid("5347ad7b-c355-46f8-aff5-909033582f63")] internal partial interface ITextRangeProvider { @@ -42,22 +37,16 @@ internal partial interface ITextRangeProvider void ExpandToEnclosingUnit(TextUnit unit); ITextRangeProvider FindAttribute(int attribute, -#if NET8_0_OR_GREATER [MarshalUsing(typeof(ComVariantMarshaller))] -#endif object value, [MarshalAs(UnmanagedType.Bool)] bool backward); ITextRangeProvider FindText( [MarshalAs(UnmanagedType.BStr)] string text, [MarshalAs(UnmanagedType.Bool)] bool backward, [MarshalAs(UnmanagedType.Bool)] bool ignoreCase); -#if NET8_0_OR_GREATER [return: MarshalUsing(typeof(ComVariantMarshaller))] -#endif object GetAttributeValue(int attribute); -#if NET8_0_OR_GREATER [return: MarshalUsing(typeof(SafeArrayMarshaller))] -#endif double[] GetBoundingRectangles(); IRawElementProviderSimple GetEnclosingElement(); [return: MarshalAs(UnmanagedType.BStr)] @@ -72,8 +61,6 @@ internal partial interface ITextRangeProvider void AddToSelection(); void RemoveFromSelection(); void ScrollIntoView([MarshalAs(UnmanagedType.Bool)] bool alignToTop); -#if NET8_0_OR_GREATER [return: MarshalUsing(typeof(SafeArrayMarshaller))] -#endif IRawElementProviderSimple[] GetChildren(); } diff --git a/src/Windows/Avalonia.Win32.Automation/Interop/IToggleProvider.cs b/src/Windows/Avalonia.Win32.Automation/Interop/IToggleProvider.cs index 85dd3c0f97..157accebee 100644 --- a/src/Windows/Avalonia.Win32.Automation/Interop/IToggleProvider.cs +++ b/src/Windows/Avalonia.Win32.Automation/Interop/IToggleProvider.cs @@ -4,12 +4,7 @@ using Avalonia.Automation.Provider; namespace Avalonia.Win32.Automation.Interop; -#if NET8_0_OR_GREATER [GeneratedComInterface(Options = ComInterfaceOptions.ManagedObjectWrapper)] -#else -[ComImport()] -[InterfaceType(ComInterfaceType.InterfaceIsIUnknown)] -#endif [Guid("56d00bd0-c4f4-433c-a836-1a52a57e0892")] internal partial interface IToggleProvider { diff --git a/src/Windows/Avalonia.Win32.Automation/Interop/ITransformProvider.cs b/src/Windows/Avalonia.Win32.Automation/Interop/ITransformProvider.cs index baabaf3664..947a850028 100644 --- a/src/Windows/Avalonia.Win32.Automation/Interop/ITransformProvider.cs +++ b/src/Windows/Avalonia.Win32.Automation/Interop/ITransformProvider.cs @@ -3,12 +3,7 @@ using System.Runtime.InteropServices.Marshalling; namespace Avalonia.Win32.Automation.Interop; -#if NET8_0_OR_GREATER [GeneratedComInterface(Options = ComInterfaceOptions.ManagedObjectWrapper)] -#else -[ComImport()] -[InterfaceType(ComInterfaceType.InterfaceIsIUnknown)] -#endif [Guid("6829ddc4-4f91-4ffa-b86f-bd3e2987cb4c")] internal partial interface ITransformProvider { diff --git a/src/Windows/Avalonia.Win32.Automation/Interop/IValueProvider.cs b/src/Windows/Avalonia.Win32.Automation/Interop/IValueProvider.cs index 6d7526c054..b86a33e4cf 100644 --- a/src/Windows/Avalonia.Win32.Automation/Interop/IValueProvider.cs +++ b/src/Windows/Avalonia.Win32.Automation/Interop/IValueProvider.cs @@ -3,12 +3,7 @@ using System.Runtime.InteropServices.Marshalling; namespace Avalonia.Win32.Automation.Interop; -#if NET8_0_OR_GREATER [GeneratedComInterface(Options = ComInterfaceOptions.ManagedObjectWrapper)] -#else -[ComImport()] -[InterfaceType(ComInterfaceType.InterfaceIsIUnknown)] -#endif [Guid("c7935180-6fb3-4201-b174-7df73adbf64a")] internal partial interface IValueProvider { diff --git a/src/Windows/Avalonia.Win32.Automation/Interop/IWindowProvider.cs b/src/Windows/Avalonia.Win32.Automation/Interop/IWindowProvider.cs index 65cec9a1f5..7f7a096aac 100644 --- a/src/Windows/Avalonia.Win32.Automation/Interop/IWindowProvider.cs +++ b/src/Windows/Avalonia.Win32.Automation/Interop/IWindowProvider.cs @@ -20,12 +20,8 @@ internal enum WindowInteractionState BlockedByModalWindow, NotResponding } -#if NET8_0_OR_GREATER + [GeneratedComInterface(Options = ComInterfaceOptions.ManagedObjectWrapper)] -#else -[ComImport()] -[InterfaceType(ComInterfaceType.InterfaceIsIUnknown)] -#endif [Guid("987df77b-db06-4d77-8f8a-86a9c3bb90b9")] internal partial interface IWindowProvider { diff --git a/src/Windows/Avalonia.Win32.Automation/Interop/UiaCoreProviderApi.cs b/src/Windows/Avalonia.Win32.Automation/Interop/UiaCoreProviderApi.cs index 36e6902846..eb2384fb5b 100644 --- a/src/Windows/Avalonia.Win32.Automation/Interop/UiaCoreProviderApi.cs +++ b/src/Windows/Avalonia.Win32.Automation/Interop/UiaCoreProviderApi.cs @@ -66,7 +66,6 @@ namespace Avalonia.Win32.Automation.Interop { public const int UIA_E_ELEMENTNOTENABLED = unchecked((int)0x80040200); -#if NET7_0_OR_GREATER [LibraryImport("UIAutomationCore.dll", StringMarshalling = StringMarshalling.Utf8)] [return: MarshalAs(UnmanagedType.Bool)] public static partial bool UiaClientsAreListening(); @@ -88,31 +87,5 @@ namespace Avalonia.Win32.Automation.Interop [LibraryImport("UIAutomationCore.dll", StringMarshalling = StringMarshalling.Utf8)] public static partial int UiaDisconnectProvider(IRawElementProviderSimple? provider); -#else - [DllImport("UIAutomationCore.dll", CharSet = CharSet.Unicode)] - public static extern bool UiaClientsAreListening(); - - [DllImport("UIAutomationCore.dll", CharSet = CharSet.Unicode)] - public static extern IntPtr UiaReturnRawElementProvider(IntPtr hwnd, IntPtr wParam, IntPtr lParam, - IRawElementProviderSimple? el); - - [DllImport("UIAutomationCore.dll", CharSet = CharSet.Unicode)] - public static extern int UiaHostProviderFromHwnd(IntPtr hwnd, - [MarshalAs(UnmanagedType.Interface)] out IRawElementProviderSimple provider); - - [DllImport("UIAutomationCore.dll", CharSet = CharSet.Unicode)] - public static extern int UiaRaiseAutomationEvent(IRawElementProviderSimple? provider, int id); - - [DllImport("UIAutomationCore.dll", CharSet = CharSet.Unicode)] - public static extern int UiaRaiseAutomationPropertyChangedEvent(IRawElementProviderSimple? provider, int id, - object? oldValue, object? newValue); - - [DllImport("UIAutomationCore.dll", CharSet = CharSet.Unicode)] - public static extern int UiaRaiseStructureChangedEvent(IRawElementProviderSimple? provider, - StructureChangeType structureChangeType, int[]? runtimeId, int runtimeIdLen); - - [DllImport("UIAutomationCore.dll", CharSet = CharSet.Unicode)] - public static extern int UiaDisconnectProvider(IRawElementProviderSimple? provider); -#endif } } diff --git a/src/Windows/Avalonia.Win32.Automation/Interop/UiaCoreTypesApi.cs b/src/Windows/Avalonia.Win32.Automation/Interop/UiaCoreTypesApi.cs index a8a83dfd57..d9cc51843e 100644 --- a/src/Windows/Avalonia.Win32.Automation/Interop/UiaCoreTypesApi.cs +++ b/src/Windows/Avalonia.Win32.Automation/Interop/UiaCoreTypesApi.cs @@ -24,37 +24,12 @@ namespace Avalonia.Win32.Automation.Interop internal const int UIA_E_NOCLICKABLEPOINT = unchecked((int)0x80040202); internal const int UIA_E_PROXYASSEMBLYNOTLOADED = unchecked((int)0x80040203); - internal static bool IsNetComInteropAvailable - { - get - { -#if NET8_0_OR_GREATER - return true; -#else -#if NET6_0_OR_GREATER - if (!System.Runtime.CompilerServices.RuntimeFeature.IsDynamicCodeSupported) - { - return false; - } -#endif - var comConfig = - AppContext.GetData("System.Runtime.InteropServices.BuiltInComInterop.IsSupported") as string; - return comConfig == null || bool.Parse(comConfig); -#endif - } - } - internal static int UiaLookupId(AutomationIdType type, ref Guid guid) { return RawUiaLookupId(type, ref guid); } -#if NET7_0_OR_GREATER [LibraryImport("UIAutomationCore.dll", EntryPoint = "UiaLookupId", StringMarshalling = StringMarshalling.Utf8)] private static partial int RawUiaLookupId(AutomationIdType type, ref Guid guid); -#else - [DllImport("UIAutomationCore.dll", EntryPoint = "UiaLookupId", CharSet = CharSet.Unicode)] - private static extern int RawUiaLookupId(AutomationIdType type, ref Guid guid); -#endif } } diff --git a/src/Windows/Avalonia.Win32.Automation/InteropAutomationNode.cs b/src/Windows/Avalonia.Win32.Automation/InteropAutomationNode.cs index d6f698513c..d97561ba11 100644 --- a/src/Windows/Avalonia.Win32.Automation/InteropAutomationNode.cs +++ b/src/Windows/Avalonia.Win32.Automation/InteropAutomationNode.cs @@ -10,11 +10,7 @@ namespace Avalonia.Win32.Automation; /// /// An automation node which serves as the root of an embedded native control automation tree. /// -#if NET8_0_OR_GREATER [GeneratedComClass] -#elif NET6_0_OR_GREATER - [RequiresUnreferencedCode("Requires .NET COM interop")] -#endif internal partial class InteropAutomationNode : AutomationNode, IRawElementProviderFragmentRoot { private readonly IntPtr _handle; diff --git a/src/Windows/Avalonia.Win32.Automation/Marshalling/ComVariant.cs b/src/Windows/Avalonia.Win32.Automation/Marshalling/ComVariant.cs index a7fcd6776c..0fa78f300e 100644 --- a/src/Windows/Avalonia.Win32.Automation/Marshalling/ComVariant.cs +++ b/src/Windows/Avalonia.Win32.Automation/Marshalling/ComVariant.cs @@ -8,7 +8,6 @@ using System.Runtime.InteropServices; namespace Avalonia.Win32.Automation.Marshalling; -#if NET7_0_OR_GREATER // Oversimplified ComVariant implementation based on https://github.com/dotnet/runtime/blob/main/src/libraries/System.Private.CoreLib/src/System/Runtime/InteropServices/Marshalling/ComVariant.cs // Available [StructLayout(LayoutKind.Explicit)] @@ -306,4 +305,3 @@ internal struct ComVariant : IDisposable private set => _typeUnion._vt = (ushort)value; } } -#endif diff --git a/src/Windows/Avalonia.Win32.Automation/Marshalling/ComVariantMarshaller.cs b/src/Windows/Avalonia.Win32.Automation/Marshalling/ComVariantMarshaller.cs index 02ae8eca28..f525e91951 100644 --- a/src/Windows/Avalonia.Win32.Automation/Marshalling/ComVariantMarshaller.cs +++ b/src/Windows/Avalonia.Win32.Automation/Marshalling/ComVariantMarshaller.cs @@ -1,5 +1,4 @@ -#if NET7_0_OR_GREATER -global using ComVariantMarshaller = Avalonia.Win32.Automation.Marshalling.ComVariantMarshaller; +global using ComVariantMarshaller = Avalonia.Win32.Automation.Marshalling.ComVariantMarshaller; using System.Runtime.InteropServices.Marshalling; namespace Avalonia.Win32.Automation.Marshalling; @@ -13,4 +12,3 @@ internal static class ComVariantMarshaller public static void Free(ComVariant unmanaged) => unmanaged.Dispose(); } -#endif diff --git a/src/Windows/Avalonia.Win32.Automation/Marshalling/SafeArrayMarshaller.cs b/src/Windows/Avalonia.Win32.Automation/Marshalling/SafeArrayMarshaller.cs index fe7fe54976..ef3af4a3b2 100644 --- a/src/Windows/Avalonia.Win32.Automation/Marshalling/SafeArrayMarshaller.cs +++ b/src/Windows/Avalonia.Win32.Automation/Marshalling/SafeArrayMarshaller.cs @@ -1,5 +1,4 @@ -#if NET7_0_OR_GREATER -using System; +using System; using System.Collections.Generic; using System.Linq; using System.Runtime.InteropServices.Marshalling; @@ -18,4 +17,3 @@ internal static class SafeArrayMarshaller where T : notnull public static void Free(SafeArrayRef unmanaged) => unmanaged.Destroy(); } -#endif diff --git a/src/Windows/Avalonia.Win32.Automation/Marshalling/SafeArrayRef.cs b/src/Windows/Avalonia.Win32.Automation/Marshalling/SafeArrayRef.cs index 14158c4ee3..f27c67e36e 100644 --- a/src/Windows/Avalonia.Win32.Automation/Marshalling/SafeArrayRef.cs +++ b/src/Windows/Avalonia.Win32.Automation/Marshalling/SafeArrayRef.cs @@ -8,13 +8,11 @@ using System.Diagnostics.CodeAnalysis; using System.Linq; using System.Runtime.CompilerServices; using System.Runtime.InteropServices; -using Avalonia.Controls.Documents; // ReSharper disable InconsistentNaming namespace Avalonia.Win32.Automation.Marshalling; #pragma warning disable CS0649 // Field is never assigned to, and will always have its default value -#if NET7_0_OR_GREATER internal unsafe partial struct SafeArrayRef { private SAFEARRAY* _ptr; @@ -340,4 +338,3 @@ internal unsafe partial struct SafeArrayRef } } } -#endif diff --git a/src/Windows/Avalonia.Win32.Automation/RootAutomationNode.cs b/src/Windows/Avalonia.Win32.Automation/RootAutomationNode.cs index 3fd59b502e..9cd4ae4850 100644 --- a/src/Windows/Avalonia.Win32.Automation/RootAutomationNode.cs +++ b/src/Windows/Avalonia.Win32.Automation/RootAutomationNode.cs @@ -1,5 +1,4 @@ using System; -using System.Diagnostics.CodeAnalysis; using System.Runtime.InteropServices; using System.Runtime.InteropServices.Marshalling; using Avalonia.Automation.Peers; @@ -9,15 +8,8 @@ using Avalonia.Win32.Automation.Interop; namespace Avalonia.Win32.Automation { -#if NET8_0_OR_GREATER [GeneratedComClass] internal partial class RootAutomationNode : AutomationNode, IRawElementProviderFragmentRoot -#else -#if NET6_0_OR_GREATER - [RequiresUnreferencedCode("Requires .NET COM interop")] -#endif - internal partial class RootAutomationNode : AutomationNode, IRawElementProviderFragmentRoot -#endif { public RootAutomationNode(AutomationPeer peer) : base(peer) diff --git a/src/Windows/Avalonia.Win32/DComposition/DirectCompositionConnection.cs b/src/Windows/Avalonia.Win32/DComposition/DirectCompositionConnection.cs index 6c73fbeb9e..55d49ca30c 100644 --- a/src/Windows/Avalonia.Win32/DComposition/DirectCompositionConnection.cs +++ b/src/Windows/Avalonia.Win32/DComposition/DirectCompositionConnection.cs @@ -21,15 +21,36 @@ internal class DirectCompositionConnection : IRenderTimer, IWindowsSurfaceFactor { private static readonly Guid IID_IDCompositionDesktopDevice = Guid.Parse("5f4633fe-1e08-4cb8-8c75-ce24333f5602"); - public event Action? Tick; + private volatile Action? _tick; public bool RunsInBackground => true; private readonly DirectCompositionShared _shared; + private readonly AutoResetEvent _wakeEvent = new(false); + private volatile bool _stopped = true; public DirectCompositionConnection(DirectCompositionShared shared) { _shared = shared; } + + public Action? Tick + { + get => _tick; + set + { + if (value != null) + { + _tick = value; + _stopped = false; + _wakeEvent.Set(); + } + else + { + _stopped = true; + _tick = null; + } + } + } private static bool TryCreateAndRegisterCore() { @@ -52,7 +73,7 @@ internal class DirectCompositionConnection : IRenderTimer, IWindowsSurfaceFactor } AvaloniaLocator.CurrentMutable.Bind().ToConstant(connect); - AvaloniaLocator.CurrentMutable.Bind().ToConstant(connect); + AvaloniaLocator.CurrentMutable.Bind().ToConstant(RenderLoop.FromTimer(connect)); tcs.SetResult(true); } catch (Exception e) @@ -81,8 +102,11 @@ internal class DirectCompositionConnection : IRenderTimer, IWindowsSurfaceFactor { try { + if (_stopped) + WaitHandle.WaitAny([_wakeEvent, cts.Token.WaitHandle]); + device.WaitForCommitCompletion(); - Tick?.Invoke(_stopwatch.Elapsed); + _tick?.Invoke(_stopwatch.Elapsed); } catch (Exception ex) { diff --git a/src/Windows/Avalonia.Win32/DirectX/DxgiConnection.cs b/src/Windows/Avalonia.Win32/DirectX/DxgiConnection.cs index 678b15e0d7..9ee2f25c86 100644 --- a/src/Windows/Avalonia.Win32/DirectX/DxgiConnection.cs +++ b/src/Windows/Avalonia.Win32/DirectX/DxgiConnection.cs @@ -2,6 +2,7 @@ using System.Collections.Generic; using System.Diagnostics; using System.Runtime.InteropServices; +using System.Threading; using System.Threading.Tasks; using Avalonia.Platform; using Avalonia.Platform.Surfaces; @@ -25,8 +26,10 @@ namespace Avalonia.Win32.DirectX public bool RunsInBackground => true; - public event Action? Tick; + private volatile Action? _tick; private readonly object _syncLock; + private readonly AutoResetEvent _wakeEvent = new(false); + private volatile bool _stopped = true; private IDXGIOutput? _output; @@ -37,6 +40,25 @@ namespace Avalonia.Win32.DirectX { _syncLock = syncLock; } + + public Action? Tick + { + get => _tick; + set + { + if (value != null) + { + _tick = value; + _stopped = false; + _wakeEvent.Set(); + } + else + { + _stopped = true; + _tick = null; + } + } + } public static bool TryCreateAndRegister() { @@ -70,6 +92,9 @@ namespace Avalonia.Win32.DirectX { try { + if (_stopped) + _wakeEvent.WaitOne(); + lock (_syncLock) { if (_output is not null) @@ -94,7 +119,7 @@ namespace Avalonia.Win32.DirectX // but theoretically someone could have a weirder setup out there DwmFlush(); } - Tick?.Invoke(_stopwatch.Elapsed); + _tick?.Invoke(_stopwatch.Elapsed); } } catch (Exception ex) @@ -199,7 +224,7 @@ namespace Avalonia.Win32.DirectX var connection = new DxgiConnection(pumpLock); AvaloniaLocator.CurrentMutable.Bind().ToConstant(connection); - AvaloniaLocator.CurrentMutable.Bind().ToConstant(connection); + AvaloniaLocator.CurrentMutable.Bind().ToConstant(RenderLoop.FromTimer(connection)); tcs.SetResult(true); connection.RunLoop(); } diff --git a/src/Windows/Avalonia.Win32/OleDataObjectHelper.cs b/src/Windows/Avalonia.Win32/OleDataObjectHelper.cs index 6104613761..198c6fca07 100644 --- a/src/Windows/Avalonia.Win32/OleDataObjectHelper.cs +++ b/src/Windows/Avalonia.Win32/OleDataObjectHelper.cs @@ -674,11 +674,7 @@ internal static class OleDataObjectHelper { var data = StringBuilderCache.GetStringAndRelease(buffer); var destSpan = new Span((void*)ptr, requiredSize); -#if NET8_0_OR_GREATER MemoryMarshal.Write(destSpan, in dropFiles); -#else - MemoryMarshal.Write(destSpan, ref dropFiles); -#endif fixed (char* sourcePtr = data) { diff --git a/src/Windows/Avalonia.Win32/Win32Platform.cs b/src/Windows/Avalonia.Win32/Win32Platform.cs index f158d539ff..7903a62d8f 100644 --- a/src/Windows/Avalonia.Win32/Win32Platform.cs +++ b/src/Windows/Avalonia.Win32/Win32Platform.cs @@ -98,7 +98,7 @@ namespace Avalonia.Win32 .Bind().ToConstant(WindowsKeyboardDevice.Instance) .Bind().ToSingleton() .Bind().ToSingleton() - .Bind().ToConstant(renderTimer) + .Bind().ToConstant(RenderLoop.FromTimer(renderTimer)) .Bind().ToConstant(s_instance) .Bind().ToConstant(new PlatformHotkeyConfiguration(KeyModifiers.Control) { diff --git a/src/Windows/Avalonia.Win32/WinRT/Composition/WinUiCompositorConnection.cs b/src/Windows/Avalonia.Win32/WinRT/Composition/WinUiCompositorConnection.cs index fa7ff2e7a3..d4cdfcbe42 100644 --- a/src/Windows/Avalonia.Win32/WinRT/Composition/WinUiCompositorConnection.cs +++ b/src/Windows/Avalonia.Win32/WinRT/Composition/WinUiCompositorConnection.cs @@ -17,8 +17,29 @@ namespace Avalonia.Win32.WinRT.Composition; internal class WinUiCompositorConnection : IRenderTimer, Win32.IWindowsSurfaceFactory { private readonly WinUiCompositionShared _shared; - public event Action? Tick; + private readonly AutoResetEvent _wakeEvent = new(false); + private volatile bool _stopped = true; + private volatile Action? _tick; public bool RunsInBackground => true; + + public Action? Tick + { + get => _tick; + set + { + if (value != null) + { + _tick = value; + _stopped = false; + _wakeEvent.Set(); + } + else + { + _stopped = true; + _tick = null; + } + } + } public WinUiCompositorConnection() { @@ -58,7 +79,7 @@ internal class WinUiCompositorConnection : IRenderTimer, Win32.IWindowsSurfaceFa }); connect = new WinUiCompositorConnection(); AvaloniaLocator.CurrentMutable.Bind().ToConstant(connect); - AvaloniaLocator.CurrentMutable.Bind().ToConstant(connect); + AvaloniaLocator.CurrentMutable.Bind().ToConstant(RenderLoop.FromTimer(connect)); tcs.SetResult(true); } @@ -102,8 +123,11 @@ internal class WinUiCompositorConnection : IRenderTimer, Win32.IWindowsSurfaceFa { _currentCommit?.Dispose(); _currentCommit = null; - _parent.Tick?.Invoke(_st.Elapsed); + _parent._tick?.Invoke(_st.Elapsed); + // Always schedule a commit so the current frame's work reaches DWM. ScheduleNextCommit(); + if (_parent._stopped) + _parent._wakeEvent.WaitOne(); } private void ScheduleNextCommit() diff --git a/src/Windows/Avalonia.Win32/WindowImpl.AppWndProc.cs b/src/Windows/Avalonia.Win32/WindowImpl.AppWndProc.cs index ab86830b73..5295e2c03a 100644 --- a/src/Windows/Avalonia.Win32/WindowImpl.AppWndProc.cs +++ b/src/Windows/Avalonia.Win32/WindowImpl.AppWndProc.cs @@ -1,8 +1,6 @@ using System; using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; -using System.Linq; -using System.Runtime.CompilerServices; using System.Runtime.InteropServices; using Avalonia.Automation.Peers; using Avalonia.Controls; @@ -155,10 +153,7 @@ namespace Avalonia.Win32 // The first and foremost thing to do - notify the TopLevel Closed?.Invoke(); - if (UiaCoreTypesApi.IsNetComInteropAvailable) - { - UiaCoreProviderApi.UiaReturnRawElementProvider(_hwnd, IntPtr.Zero, IntPtr.Zero, null); - } + UiaCoreProviderApi.UiaReturnRawElementProvider(_hwnd, IntPtr.Zero, IntPtr.Zero, null); // We need to release IMM context and state to avoid leaks. if (Imm32InputMethod.Current.Hwnd == _hwnd) @@ -224,7 +219,7 @@ namespace Avalonia.Win32 } var requestIcon = (Icons)wParam; - var requestDpi = (uint) lParam; + var requestDpi = (uint)lParam; if (requestDpi == 0) { @@ -937,7 +932,7 @@ namespace Avalonia.Win32 return IntPtr.Zero; } case WindowsMessage.WM_GETOBJECT: - if ((long)lParam == uiaRootObjectId && UiaCoreTypesApi.IsNetComInteropAvailable && _owner?.FocusRoot is Control control) + if ((long)lParam == uiaRootObjectId && _owner?.FocusRoot is Control control) { var peer = ControlAutomationPeer.CreatePeerForElement(control); var node = AutomationNode.GetOrCreate(peer); @@ -946,7 +941,7 @@ namespace Avalonia.Win32 break; case WindowsMessage.WM_WINDOWPOSCHANGED: var winPos = Marshal.PtrToStructure(lParam); - if((winPos.flags & (uint)SetWindowPosFlags.SWP_SHOWWINDOW) != 0) + if ((winPos.flags & (uint)SetWindowPosFlags.SWP_SHOWWINDOW) != 0) { OnShowHideMessage(true); } @@ -973,7 +968,7 @@ namespace Avalonia.Win32 if (message == WindowsMessage.WM_KEYDOWN) { - if(e is RawKeyEventArgs args && args.Key == Key.ImeProcessed) + if (e is RawKeyEventArgs args && args.Key == Key.ImeProcessed) { _ignoreWmChar = true; } @@ -1118,7 +1113,7 @@ namespace Avalonia.Win32 var x = mp.x > 32767 ? mp.x - 65536 : mp.x; var y = mp.y > 32767 ? mp.y - 65536 : mp.y; - if(mp.time <= prevMovePoint.time || mp.time >= movePoint.time) + if (mp.time <= prevMovePoint.time || mp.time >= movePoint.time) continue; s_sortedPoints.Add(new InternalPoint diff --git a/src/iOS/Avalonia.iOS/DisplayLinkTimer.cs b/src/iOS/Avalonia.iOS/DisplayLinkTimer.cs index 676554811e..e8a313afa8 100644 --- a/src/iOS/Avalonia.iOS/DisplayLinkTimer.cs +++ b/src/iOS/Avalonia.iOS/DisplayLinkTimer.cs @@ -1,7 +1,6 @@ using System; using System.Diagnostics; using System.Threading; -using System.Threading.Tasks; using Avalonia.Rendering; using CoreAnimation; using Foundation; @@ -11,7 +10,7 @@ namespace Avalonia.iOS { class DisplayLinkTimer : IRenderTimer { - public event Action? Tick; + private volatile Action? _tick; private Stopwatch _st = Stopwatch.StartNew(); public DisplayLinkTimer() @@ -31,9 +30,16 @@ namespace Avalonia.iOS public bool RunsInBackground => true; + // TODO: start/stop on RenderLoop request + public Action? Tick + { + get => _tick; + set => _tick = value; + } + private void OnLinkTick() { - Tick?.Invoke(_st.Elapsed); + _tick?.Invoke(_st.Elapsed); } } } diff --git a/src/iOS/Avalonia.iOS/Platform.cs b/src/iOS/Avalonia.iOS/Platform.cs index 29633a8609..79926a3836 100644 --- a/src/iOS/Avalonia.iOS/Platform.cs +++ b/src/iOS/Avalonia.iOS/Platform.cs @@ -93,7 +93,7 @@ namespace Avalonia.iOS { Key.PageUp , "⇞" }, { Key.Right , "→" }, { Key.Space , "␣" }, { Key.Tab , "⇥" }, { Key.Up , "↑" } }, ctrl: "⌃", meta: "⌘", shift: "⇧", alt: "⌥")) - .Bind().ToConstant(Timer) + .Bind().ToConstant(RenderLoop.FromTimer(Timer)) .Bind().ToConstant(keyboard); if (appDelegate is not null) diff --git a/src/tools/Avalonia.Designer.HostApp/Avalonia.Designer.HostApp.csproj b/src/tools/Avalonia.Designer.HostApp/Avalonia.Designer.HostApp.csproj index e4b1d0baf7..f70c46b8c2 100644 --- a/src/tools/Avalonia.Designer.HostApp/Avalonia.Designer.HostApp.csproj +++ b/src/tools/Avalonia.Designer.HostApp/Avalonia.Designer.HostApp.csproj @@ -14,7 +14,6 @@ - diff --git a/src/tools/Avalonia.Designer.HostApp/TinyJson.cs b/src/tools/Avalonia.Designer.HostApp/TinyJson.cs index d48475887c..8c8b8282d7 100644 --- a/src/tools/Avalonia.Designer.HostApp/TinyJson.cs +++ b/src/tools/Avalonia.Designer.HostApp/TinyJson.cs @@ -344,11 +344,7 @@ namespace TinyJson static object ParseObject(Type type, string json) { -#if NET6_0_OR_GREATER object instance = RuntimeHelpers.GetUninitializedObject(type); -#else - object instance = FormatterServices.GetUninitializedObject(type); -#endif //The list is split into key/value pairs only, this means the split must be divisible by 2 to be valid JSON List elems = Split(json); diff --git a/tests/Avalonia.Base.UnitTests/Animation/AnimationIterationTests.cs b/tests/Avalonia.Base.UnitTests/Animation/AnimationIterationTests.cs index 4a3d67d0c7..752c1b166b 100644 --- a/tests/Avalonia.Base.UnitTests/Animation/AnimationIterationTests.cs +++ b/tests/Avalonia.Base.UnitTests/Animation/AnimationIterationTests.cs @@ -93,6 +93,256 @@ namespace Avalonia.Base.UnitTests.Animation Assert.True(animationRun.Status == TaskStatus.RanToCompletion); Assert.Equal(border.Width, 100d); } + + [Fact] + public void Pause_Animation_When_IsEffectivelyVisible_Is_False() + { + var keyframe1 = new KeyFrame() + { + Setters = { new Setter(Layoutable.WidthProperty, 200d), }, Cue = new Cue(1d) + }; + var keyframe2 = new KeyFrame() + { + Setters = { new Setter(Layoutable.WidthProperty, 150d), }, Cue = new Cue(0.5d) + }; + + var keyframe3 = new KeyFrame() + { + Setters = { new Setter(Layoutable.WidthProperty, 100d), }, Cue = new Cue(0d) + }; + + var animation = new Animation() + { + Duration = TimeSpan.FromSeconds(3), + Delay = TimeSpan.FromSeconds(3), + DelayBetweenIterations = TimeSpan.FromSeconds(3), + IterationCount = new IterationCount(2), + Children = { keyframe1, keyframe2, keyframe3 } + }; + + var border = new Border() { Height = 100d, Width = 100d }; + + var clock = new TestClock(); + var animationRun = animation.RunAsync(border, clock, TestContext.Current.CancellationToken); + + border.Measure(Size.Infinity); + border.Arrange(new Rect(border.DesiredSize)); + + clock.Step(TimeSpan.Zero); + + clock.Step(TimeSpan.FromSeconds(0)); + Assert.Equal(100d, border.Width); + + // Hide the border — this should pause the animation clock. + border.IsVisible = false; + + clock.Step(TimeSpan.FromSeconds(4.5)); + + // Width should not change while invisible (animation is paused). + Assert.Equal(100d, border.Width); + + // Show the border — animation resumes from where it left off. + border.IsVisible = true; + + // The pause absorbed 4.5s of wall-clock time, so internal time = wall - 4.5. + // To reach internal time 6s (end of iteration 1, cue 1.0 -> width 200): + // wall = 4.5 + 6 = 10.5 + clock.Step(TimeSpan.FromSeconds(10.5)); + Assert.Equal(200d, border.Width); + + // To complete the animation (internal time 14s triggers trailing delay of iter 2): + // wall = 4.5 + 14 = 18.5 + clock.Step(TimeSpan.FromSeconds(18.5)); + Assert.True(animationRun.Status == TaskStatus.RanToCompletion); + Assert.Equal(100d, border.Width); + } + + [Fact] + public void Pause_Animation_When_IsEffectivelyVisible_Is_False_Nested() + { + var keyframe1 = new KeyFrame() + { + Setters = { new Setter(Layoutable.WidthProperty, 200d), }, Cue = new Cue(1d) + }; + var keyframe2 = new KeyFrame() + { + Setters = { new Setter(Layoutable.WidthProperty, 150d), }, Cue = new Cue(0.5d) + }; + + var keyframe3 = new KeyFrame() + { + Setters = { new Setter(Layoutable.WidthProperty, 100d), }, Cue = new Cue(0d) + }; + + var animation = new Animation() + { + Duration = TimeSpan.FromSeconds(3), + Delay = TimeSpan.FromSeconds(3), + DelayBetweenIterations = TimeSpan.FromSeconds(3), + IterationCount = new IterationCount(2), + Children = { keyframe1, keyframe2, keyframe3 } + }; + + var border = new Border() { Height = 100d, Width = 100d }; + + var borderParent = new Border { Child = border }; + + var clock = new TestClock(); + var animationRun = animation.RunAsync(border, clock, TestContext.Current.CancellationToken); + + border.Measure(Size.Infinity); + border.Arrange(new Rect(border.DesiredSize)); + + clock.Step(TimeSpan.Zero); + + clock.Step(TimeSpan.FromSeconds(0)); + Assert.Equal(100d, border.Width); + + // Hide the parent — this makes border.IsEffectivelyVisible false, + // which should pause the animation clock. + borderParent.IsVisible = false; + + clock.Step(TimeSpan.FromSeconds(4.5)); + + // Width should not change while parent is invisible (animation is paused). + Assert.Equal(100d, border.Width); + + // Show the parent — animation resumes from where it left off. + borderParent.IsVisible = true; + + // The pause absorbed 4.5s of wall-clock time, so internal time = wall - 4.5. + // To reach internal time 6s (end of iteration 1, cue 1.0 -> width 200): + // wall = 4.5 + 6 = 10.5 + clock.Step(TimeSpan.FromSeconds(10.5)); + Assert.Equal(200d, border.Width); + + // To complete the animation (internal time 14s triggers trailing delay of iter 2): + // wall = 4.5 + 14 = 18.5 + clock.Step(TimeSpan.FromSeconds(18.5)); + Assert.True(animationRun.Status == TaskStatus.RanToCompletion); + Assert.Equal(100d, border.Width); + } + + [Fact] + public void Stop_And_Dispose_Animation_When_Detached_From_Visual_Tree() + { + var keyframe1 = new KeyFrame() + { + Setters = { new Setter(Layoutable.WidthProperty, 200d), }, Cue = new Cue(1d) + }; + var keyframe2 = new KeyFrame() + { + Setters = { new Setter(Layoutable.WidthProperty, 100d), }, Cue = new Cue(0d) + }; + var animation = new Animation() + { + Duration = TimeSpan.FromSeconds(10), + IterationCount = new IterationCount(1), + Children = { keyframe2, keyframe1 } + }; + var border = new Border() { Height = 100d, Width = 50d }; + var root = new TestRoot(border); + var clock = new TestClock(); + var animationRun = animation.RunAsync(border, clock, TestContext.Current.CancellationToken); + clock.Step(TimeSpan.Zero); + clock.Step(TimeSpan.FromSeconds(0)); + Assert.False(animationRun.IsCompleted); + + // Detach from visual tree + root.Child = null; + + // Animation should be completed/disposed + Assert.True(animationRun.IsCompleted); + + // Further clock ticks should not affect the border + var widthAfterDetach = border.Width; + clock.Step(TimeSpan.FromSeconds(5)); + clock.Step(TimeSpan.FromSeconds(10)); + Assert.Equal(widthAfterDetach, border.Width); + } + + [Fact] + public void Pause_Animation_When_Control_Starts_Invisible() + { + var keyframe1 = new KeyFrame() + { + Setters = { new Setter(Layoutable.WidthProperty, 200d), }, Cue = new Cue(1d) + }; + var keyframe2 = new KeyFrame() + { + Setters = { new Setter(Layoutable.WidthProperty, 100d), }, Cue = new Cue(0d) + }; + var animation = new Animation() + { + Duration = TimeSpan.FromSeconds(3), + IterationCount = new IterationCount(1), + Children = { keyframe2, keyframe1 } + }; + + var border = new Border() { Height = 100d, Width = 100d, IsVisible = false }; + var clock = new TestClock(); + var animationRun = animation.RunAsync(border, clock, TestContext.Current.CancellationToken); + + // Clock ticks while invisible should not advance the animation. + clock.Step(TimeSpan.Zero); + clock.Step(TimeSpan.FromSeconds(1)); + clock.Step(TimeSpan.FromSeconds(2)); + Assert.Equal(100d, border.Width); + Assert.False(animationRun.IsCompleted); + + // Make visible — animation starts from the beginning. + border.IsVisible = true; + + // The pause absorbed 2s of wall-clock time, so to reach internal time 3s: + // wall = 2 + 3 = 5 + clock.Step(TimeSpan.FromSeconds(5)); + Assert.True(animationRun.IsCompleted); + Assert.Equal(100d, border.Width); + } + + [Fact] + public void Animation_Plays_Correctly_After_Reattach() + { + var keyframe1 = new KeyFrame() + { + Setters = { new Setter(Layoutable.WidthProperty, 200d), }, Cue = new Cue(1d) + }; + var keyframe2 = new KeyFrame() + { + Setters = { new Setter(Layoutable.WidthProperty, 100d), }, Cue = new Cue(0d) + }; + var animation = new Animation() + { + Duration = TimeSpan.FromSeconds(5), + IterationCount = new IterationCount(1), + FillMode = FillMode.Forward, + Children = { keyframe2, keyframe1 } + }; + + var border = new Border() { Height = 100d, Width = 50d }; + var root = new TestRoot(border); + var clock = new TestClock(); + var animationRun = animation.RunAsync(border, clock, TestContext.Current.CancellationToken); + + clock.Step(TimeSpan.Zero); + Assert.False(animationRun.IsCompleted); + + // Detach — animation completes. + root.Child = null; + Assert.True(animationRun.IsCompleted); + + // Reattach and start a fresh animation. + root.Child = border; + var clock2 = new TestClock(); + var animationRun2 = animation.RunAsync(border, clock2, TestContext.Current.CancellationToken); + + clock2.Step(TimeSpan.Zero); + Assert.False(animationRun2.IsCompleted); + + clock2.Step(TimeSpan.FromSeconds(5)); + Assert.True(animationRun2.IsCompleted); + Assert.Equal(200d, border.Width); + } [Fact] public void Check_FillModes_Start_and_End_Values_if_Retained() diff --git a/tests/Avalonia.Base.UnitTests/Composition/CompositionAnimationTests.cs b/tests/Avalonia.Base.UnitTests/Composition/CompositionAnimationTests.cs index cf6d7a8aee..21ac9c1ae1 100644 --- a/tests/Avalonia.Base.UnitTests/Composition/CompositionAnimationTests.cs +++ b/tests/Avalonia.Base.UnitTests/Composition/CompositionAnimationTests.cs @@ -88,7 +88,7 @@ public class CompositionAnimationTests : ScopedTestBase { using var scope = AvaloniaLocator.EnterScope(); var compositor = - new Compositor(new RenderLoop(new CompositorTestServices.ManualRenderTimer()), null); + new Compositor(RenderLoop.FromTimer(new CompositorTestServices.ManualRenderTimer()), null); var target = compositor.CreateSolidColorVisual(); var ani = new ScalarKeyFrameAnimation(compositor); foreach (var frame in data.Frames) diff --git a/tests/Avalonia.Base.UnitTests/Input/InputElement_Focus.cs b/tests/Avalonia.Base.UnitTests/Input/InputElement_Focus.cs index 7755eb80cf..cdb4588fff 100644 --- a/tests/Avalonia.Base.UnitTests/Input/InputElement_Focus.cs +++ b/tests/Avalonia.Base.UnitTests/Input/InputElement_Focus.cs @@ -577,7 +577,7 @@ namespace Avalonia.Base.UnitTests.Input }; target.Focus(); - root.FocusManager.ClearFocus(); + root.FocusManager.Focus(null); Assert.Null(root.FocusManager.GetFocusedElement()); } diff --git a/tests/Avalonia.Base.UnitTests/Input/KeyboardDeviceTests.cs b/tests/Avalonia.Base.UnitTests/Input/KeyboardDeviceTests.cs index d11872ba6a..b1446d961f 100644 --- a/tests/Avalonia.Base.UnitTests/Input/KeyboardDeviceTests.cs +++ b/tests/Avalonia.Base.UnitTests/Input/KeyboardDeviceTests.cs @@ -15,7 +15,7 @@ namespace Avalonia.Base.UnitTests.Input using (UnitTestApplication.Start(TestServices.FocusableWindow)) { var window = new Window(); - window.FocusManager.ClearFocus(); + window.FocusManager.Focus(null); int raised = 0; window.KeyDown += (sender, ev) => { @@ -71,7 +71,7 @@ namespace Avalonia.Base.UnitTests.Input using (UnitTestApplication.Start(TestServices.FocusableWindow)) { var window = new Window(); - window.FocusManager.ClearFocus(); + window.FocusManager.Focus(null); int raised = 0; window.TextInput += (sender, ev) => { diff --git a/tests/Avalonia.Base.UnitTests/Rendering/CompositorInvalidationTests.cs b/tests/Avalonia.Base.UnitTests/Rendering/CompositorInvalidationTests.cs index 699f450223..ef0e01a104 100644 --- a/tests/Avalonia.Base.UnitTests/Rendering/CompositorInvalidationTests.cs +++ b/tests/Avalonia.Base.UnitTests/Rendering/CompositorInvalidationTests.cs @@ -38,6 +38,31 @@ public class CompositorInvalidationTests : CompositorTestsBase s.AssertRects(new Rect(30, 50, 20, 10)); } } + + [Fact] + public void Sibling_Controls_Should_Invalidate_Union_Rect_When_Removed() + { + using (var s = new CompositorCanvas()) + { + var control = new Border() + { + Background = Brushes.Red, Width = 20, Height = 10, + [Canvas.LeftProperty] = 30, [Canvas.TopProperty] = 10 + }; + var control2 = new Border() + { + Background = Brushes.Blue, Width = 20, Height = 10, + [Canvas.LeftProperty] = 30, [Canvas.TopProperty] = 50 + }; + s.Canvas.Children.Add(control); + s.Canvas.Children.Add(control2); + s.RunJobs(); + s.Events.Rects.Clear(); + s.Canvas.Children.Remove(control); + s.Canvas.Children.Remove(control2); + s.AssertRects(new Rect(30, 10, 20, 50)); + } + } [Fact] public void Control_Should_Invalidate_Both_Own_Rects_When_Moved() diff --git a/tests/Avalonia.Benchmarks/Compositor/CompositionTargetUpdate.cs b/tests/Avalonia.Benchmarks/Compositor/CompositionTargetUpdate.cs index 7451e3c843..06df626857 100644 --- a/tests/Avalonia.Benchmarks/Compositor/CompositionTargetUpdate.cs +++ b/tests/Avalonia.Benchmarks/Compositor/CompositionTargetUpdate.cs @@ -22,7 +22,7 @@ public class CompositionTargetUpdateOnly : IDisposable class Timer : IRenderTimer { - event Action IRenderTimer.Tick { add { } remove { } } + public Action Tick { get; set; } = null!; public bool RunsInBackground => false; } @@ -52,7 +52,7 @@ public class CompositionTargetUpdateOnly : IDisposable { _includeRender = includeRender; _app = UnitTestApplication.Start(TestServices.StyledWindow); - _compositor = new Compositor(new RenderLoop(new Timer()), null, true, new ManualScheduler(), true, + _compositor = new Compositor(RenderLoop.FromTimer(new Timer()), null, true, new ManualScheduler(), true, Dispatcher.UIThread, null); _target = _compositor.CreateCompositionTarget(() => [new NullFramebuffer()]); _target.PixelSize = new PixelSize(1000, 1000); @@ -99,7 +99,7 @@ public class CompositionTargetUpdateOnly : IDisposable { _target.Root.Offset = new Vector3D(_target.Root.Offset.X == 0 ? 1 : 0, 0, 0); _compositor.Commit(); - _compositor.Server.Render(); + _compositor.Server.Render(false); if (!_includeRender) _target.Server.Update(); diff --git a/tests/Avalonia.Controls.UnitTests/ListBoxTests_Single.cs b/tests/Avalonia.Controls.UnitTests/ListBoxTests_Single.cs index f0f9b820f7..1c2d43e94f 100644 --- a/tests/Avalonia.Controls.UnitTests/ListBoxTests_Single.cs +++ b/tests/Avalonia.Controls.UnitTests/ListBoxTests_Single.cs @@ -35,7 +35,7 @@ namespace Avalonia.Controls.UnitTests Prepare(target); - target.Presenter!.Panel!.Children[0].RaiseEvent(new GotFocusEventArgs + target.Presenter!.Panel!.Children[0].RaiseEvent(new FocusChangedEventArgs(InputElement.GotFocusEvent) { NavigationMethod = NavigationMethod.Tab, }); @@ -57,7 +57,7 @@ namespace Avalonia.Controls.UnitTests AvaloniaLocator.CurrentMutable.Bind().ToConstant(new PlatformHotkeyConfiguration()); Prepare(target); - target.Presenter!.Panel!.Children[0].RaiseEvent(new GotFocusEventArgs + target.Presenter!.Panel!.Children[0].RaiseEvent(new FocusChangedEventArgs(InputElement.GotFocusEvent) { NavigationMethod = NavigationMethod.Directional, KeyModifiers = KeyModifiers.Control diff --git a/tests/Avalonia.Controls.UnitTests/NavigationPageTests.cs b/tests/Avalonia.Controls.UnitTests/NavigationPageTests.cs index 8f3055a2fd..9602256fe8 100644 --- a/tests/Avalonia.Controls.UnitTests/NavigationPageTests.cs +++ b/tests/Avalonia.Controls.UnitTests/NavigationPageTests.cs @@ -1,7 +1,11 @@ using System; using System.Collections.Generic; +using System.Threading; using System.Threading.Tasks; using Avalonia.Animation; +using Avalonia.Controls.Presenters; +using Avalonia.Controls.Primitives; +using Avalonia.Controls.Templates; using Avalonia.Interactivity; using Avalonia.LogicalTree; using Avalonia.UnitTests; @@ -1574,4 +1578,164 @@ public class NavigationPageTests } } + public class LifecycleAfterTransitionTests : ScopedTestBase + { + [Fact] + public async Task PushAsync_LifecycleEvents_FireAfterTransition() + { + var tcs = new TaskCompletionSource(); + var transition = new ControllableTransition(tcs.Task); + var nav = CreateNavigationPage(transition); + + var root = new ContentPage { Header = "Root" }; + await nav.PushAsync(root); + + bool navigatedFromDuringTransition = false; + bool navigatedToDuringTransition = false; + bool pushedDuringTransition = false; + + var second = new ContentPage { Header = "Second" }; + root.NavigatedFrom += (_, _) => navigatedFromDuringTransition = !tcs.Task.IsCompleted; + second.NavigatedTo += (_, _) => navigatedToDuringTransition = !tcs.Task.IsCompleted; + nav.Pushed += (_, _) => pushedDuringTransition = !tcs.Task.IsCompleted; + + var pushTask = nav.PushAsync(second); + + tcs.SetResult(); + await pushTask; + + Assert.False(navigatedFromDuringTransition); + Assert.False(navigatedToDuringTransition); + Assert.False(pushedDuringTransition); + } + + [Fact] + public async Task PopAsync_LifecycleEvents_FireAfterTransition() + { + var tcs = new TaskCompletionSource(); + var nav = CreateNavigationPage(null); + + var root = new ContentPage { Header = "Root" }; + var top = new ContentPage { Header = "Top" }; + await nav.PushAsync(root); + await nav.PushAsync(top); + + nav.PageTransition = new ControllableTransition(tcs.Task); + + bool navigatedFromDuringTransition = false; + bool navigatedToDuringTransition = false; + bool poppedDuringTransition = false; + + top.NavigatedFrom += (_, _) => navigatedFromDuringTransition = !tcs.Task.IsCompleted; + root.NavigatedTo += (_, _) => navigatedToDuringTransition = !tcs.Task.IsCompleted; + nav.Popped += (_, _) => poppedDuringTransition = !tcs.Task.IsCompleted; + + var popTask = nav.PopAsync(); + + tcs.SetResult(); + await popTask; + + Assert.False(navigatedFromDuringTransition); + Assert.False(navigatedToDuringTransition); + Assert.False(poppedDuringTransition); + } + + [Fact] + public async Task PopToRootAsync_LifecycleEvents_FireAfterTransition() + { + var tcs = new TaskCompletionSource(); + var nav = CreateNavigationPage(null); + + var root = new ContentPage { Header = "Root" }; + var second = new ContentPage { Header = "Second" }; + var third = new ContentPage { Header = "Third" }; + await nav.PushAsync(root); + await nav.PushAsync(second); + await nav.PushAsync(third); + + nav.PageTransition = new ControllableTransition(tcs.Task); + + bool navigatedFromDuringTransition = false; + bool navigatedToDuringTransition = false; + bool poppedToRootDuringTransition = false; + + second.NavigatedFrom += (_, _) => navigatedFromDuringTransition = !tcs.Task.IsCompleted; + third.NavigatedFrom += (_, _) => navigatedFromDuringTransition = !tcs.Task.IsCompleted; + root.NavigatedTo += (_, _) => navigatedToDuringTransition = !tcs.Task.IsCompleted; + nav.PoppedToRoot += (_, _) => poppedToRootDuringTransition = !tcs.Task.IsCompleted; + + var popTask = nav.PopToRootAsync(); + + tcs.SetResult(); + await popTask; + + Assert.False(navigatedFromDuringTransition); + Assert.False(navigatedToDuringTransition); + Assert.False(poppedToRootDuringTransition); + } + + private static NavigationPage CreateNavigationPage(IPageTransition? transition) + { + var nav = new NavigationPage + { + PageTransition = transition, + Template = NavigationPageTemplate() + }; + var root = new TestRoot { Child = nav }; + root.LayoutManager.ExecuteInitialLayoutPass(); + return nav; + } + + private static IControlTemplate NavigationPageTemplate() + { + return new FuncControlTemplate((parent, ns) => + { + var contentHost = new Panel + { + Name = "PART_ContentHost", + Children = + { + new ContentPresenter { Name = "PART_PageBackPresenter" }.RegisterInNameScope(ns), + new ContentPresenter { Name = "PART_PagePresenter" }.RegisterInNameScope(ns), + } + }.RegisterInNameScope(ns); + + return new Panel + { + Children = + { + new Border + { + Name = "PART_NavigationBar", + Child = new Button { Name = "PART_BackButton" }.RegisterInNameScope(ns) + }.RegisterInNameScope(ns), + contentHost, + new ContentPresenter { Name = "PART_TopCommandBar" }.RegisterInNameScope(ns), + new ContentPresenter { Name = "PART_ModalBackPresenter" }.RegisterInNameScope(ns), + new ContentPresenter { Name = "PART_ModalPresenter" }.RegisterInNameScope(ns), + } + }; + }); + } + + private class ControllableTransition : IPageTransition + { + private readonly Task _gate; + + public ControllableTransition(Task gate) + { + _gate = gate; + } + + public async Task Start(Visual? from, Visual? to, bool forward, CancellationToken cancellationToken) + { + if (to != null) + to.IsVisible = true; + await _gate; + if (from != null) + from.IsVisible = false; + } + } + } + } diff --git a/tests/Avalonia.Controls.UnitTests/PipsPagerTests.cs b/tests/Avalonia.Controls.UnitTests/PipsPagerTests.cs new file mode 100644 index 0000000000..ffc0469ce3 --- /dev/null +++ b/tests/Avalonia.Controls.UnitTests/PipsPagerTests.cs @@ -0,0 +1,578 @@ +using Avalonia.Input; +using Avalonia.UnitTests; +using Avalonia.VisualTree; +using Avalonia.Interactivity; +using Avalonia.Layout; +using Avalonia.Controls.Primitives; +using Avalonia.Controls.Templates; +using System.Linq; +using Xunit; + +namespace Avalonia.Controls.UnitTests +{ + public class PipsPagerTests : ScopedTestBase + { + [Fact] + public void NumberOfPages_Should_Update_Pips() + { + var target = new PipsPager(); + + target.NumberOfPages = 5; + + Assert.Equal(5, target.TemplateSettings.Pips.Count); + Assert.Equal(1, target.TemplateSettings.Pips[0]); + Assert.Equal(5, target.TemplateSettings.Pips[4]); + } + + [Fact] + public void Decreasing_NumberOfPages_Should_Update_Pips() + { + var target = new PipsPager(); + target.NumberOfPages = 5; + + target.NumberOfPages = 3; + + Assert.Equal(3, target.TemplateSettings.Pips.Count); + } + + [Fact] + public void Decreasing_NumberOfPages_Should_Update_SelectedPageIndex() + { + var target = new PipsPager(); + target.NumberOfPages = 5; + target.SelectedPageIndex = 4; + + target.NumberOfPages = 3; + + Assert.Equal(2, target.SelectedPageIndex); + } + + [Fact] + public void SelectedPageIndex_Should_Be_Clamped_To_Zero() + { + var target = new PipsPager(); + target.NumberOfPages = 5; + + target.SelectedPageIndex = -1; + + Assert.Equal(0, target.SelectedPageIndex); + } + + [Fact] + public void SelectedPageIndex_Change_Should_Raise_Event() + { + var target = new PipsPager(); + target.NumberOfPages = 5; + var raised = false; + target.SelectedIndexChanged += (s, e) => raised = true; + + target.SelectedPageIndex = 2; + + Assert.True(raised); + } + + [Fact] + public void Next_Button_Should_Increment_Index() + { + using var unittestApplication = UnitTestApplication.Start(TestServices.StyledWindow); + + var target = new PipsPager + { + NumberOfPages = 5, + SelectedPageIndex = 1, + IsNextButtonVisible = true, + Template = GetTemplate() + }; + + var root = new TestRoot(target); + target.ApplyTemplate(); + + var nextButton = target.GetVisualDescendants().OfType