diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 8e00cbd46e..962d8b80f7 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -19,6 +19,9 @@ env: DOTNET_CLI_TELEMETRY_OPTOUT: true DOTNET_SKIP_FIRST_TIME_EXPERIENCE: true +permissions: + contents: read + jobs: build: name: Build (${{ matrix.os }}) @@ -42,10 +45,10 @@ jobs: - name: Determine version suffix shell: bash run: | - if [[ "${{ github.ref }}" == refs/heads/release/* ]]; then - echo "VERSION_SUFFIX=" >> $GITHUB_ENV + if [[ "$GITHUB_REF" == refs/heads/release/* ]]; then + echo "VERSION_SUFFIX=" >> "$GITHUB_ENV" else - echo "VERSION_SUFFIX=-build.${{ github.run_number }}" >> $GITHUB_ENV + echo "VERSION_SUFFIX=-build.${GITHUB_RUN_NUMBER}" >> "$GITHUB_ENV" fi - name: Restore @@ -58,6 +61,9 @@ jobs: name: Test (${{ matrix.os }}) runs-on: ${{ matrix.os }} needs: build + permissions: + contents: read + checks: write strategy: fail-fast: false matrix: @@ -76,10 +82,10 @@ jobs: - name: Determine version suffix shell: bash run: | - if [[ "${{ github.ref }}" == refs/heads/release/* ]]; then - echo "VERSION_SUFFIX=" >> $GITHUB_ENV + if [[ "$GITHUB_REF" == refs/heads/release/* ]]; then + echo "VERSION_SUFFIX=" >> "$GITHUB_ENV" else - echo "VERSION_SUFFIX=-build.${{ github.run_number }}" >> $GITHUB_ENV + echo "VERSION_SUFFIX=-build.${GITHUB_RUN_NUMBER}" >> "$GITHUB_ENV" fi - name: Restore @@ -102,7 +108,8 @@ jobs: - name: Publish test results uses: dorny/test-reporter@v1 - if: always() + # Fork and Dependabot PR tokens cannot create check runs; TRX artifacts still upload above. + if: ${{ always() && (github.event_name != 'pull_request' || (github.event.pull_request.head.repo.full_name == github.repository && github.actor != 'dependabot[bot]')) }} with: name: Test Results (${{ matrix.os }}) path: artifacts/test/**/*.trx @@ -131,10 +138,10 @@ jobs: - name: Determine version suffix shell: bash run: | - if [[ "${{ github.ref }}" == refs/heads/release/* ]]; then - echo "VERSION_SUFFIX=" >> $GITHUB_ENV + if [[ "$GITHUB_REF" == refs/heads/release/* ]]; then + echo "VERSION_SUFFIX=" >> "$GITHUB_ENV" else - echo "VERSION_SUFFIX=-build.${{ github.run_number }}" >> $GITHUB_ENV + echo "VERSION_SUFFIX=-build.${GITHUB_RUN_NUMBER}" >> "$GITHUB_ENV" fi - name: Restore library @@ -167,10 +174,10 @@ jobs: - name: Determine version suffix shell: bash run: | - if [[ "${{ github.ref }}" == refs/heads/release/* ]]; then - echo "VERSION_SUFFIX=" >> $GITHUB_ENV + if [[ "$GITHUB_REF" == refs/heads/release/* ]]; then + echo "VERSION_SUFFIX=" >> "$GITHUB_ENV" else - echo "VERSION_SUFFIX=-build.${{ github.run_number }}" >> $GITHUB_ENV + echo "VERSION_SUFFIX=-build.${GITHUB_RUN_NUMBER}" >> "$GITHUB_ENV" fi - name: Restore @@ -211,10 +218,10 @@ jobs: - name: Determine version suffix shell: bash run: | - if [[ "${{ github.ref }}" == refs/heads/release/* ]]; then - echo "VERSION_SUFFIX=" >> $GITHUB_ENV + if [[ "$GITHUB_REF" == refs/heads/release/* ]]; then + echo "VERSION_SUFFIX=" >> "$GITHUB_ENV" else - echo "VERSION_SUFFIX=-build.${{ github.run_number }}" >> $GITHUB_ENV + echo "VERSION_SUFFIX=-build.${GITHUB_RUN_NUMBER}" >> "$GITHUB_ENV" fi - name: Restore diff --git a/src/Svg.Controls.Skia.Avalonia/Svg.cs b/src/Svg.Controls.Skia.Avalonia/Svg.cs index a7c28c5371..e7345b308b 100644 --- a/src/Svg.Controls.Skia.Avalonia/Svg.cs +++ b/src/Svg.Controls.Skia.Avalonia/Svg.cs @@ -888,7 +888,6 @@ private void DispatchPointerPressed(PointerPressedEventArgs e) return; } - e.Pointer.Capture(this); var currentPoint = e.GetCurrentPoint(this); var button = MapPointerUpdateKind(currentPoint.Properties.PointerUpdateKind); var result = Interaction.DispatchPointerPressed(skSvg, CreatePointerInput(e, button, e.ClickCount, 0)); @@ -908,7 +907,17 @@ private void DispatchPointerReleased(PointerReleasedEventArgs e) var result = Interaction.DispatchPointerReleased(skSvg, CreatePointerInput(e, button, 0, 0)); e.Handled |= result.Handled; ApplyNativeCursor(result.Cursor); - e.Pointer.Capture(null); + } + + protected override void OnPointerCaptureLost(PointerCaptureLostEventArgs e) + { + base.OnPointerCaptureLost(e); + + if (Interaction.CapturedElement is not null || Interaction.PressedElement is not null) + { + Interaction.Reset(); + ApplyNativeCursor(null); + } } private void DispatchPointerWheelChanged(PointerWheelEventArgs e) diff --git a/tests/Svg.Controls.Skia.Avalonia.UnitTests/SvgControlTests.cs b/tests/Svg.Controls.Skia.Avalonia.UnitTests/SvgControlTests.cs index f62a7f4ae8..2289e72f8a 100644 --- a/tests/Svg.Controls.Skia.Avalonia.UnitTests/SvgControlTests.cs +++ b/tests/Svg.Controls.Skia.Avalonia.UnitTests/SvgControlTests.cs @@ -1,6 +1,11 @@ using System; using System.Reflection; +using Avalonia; +using Avalonia.Controls; +using Avalonia.Headless; using Avalonia.Headless.XUnit; +using Avalonia.Input; +using Avalonia.Interactivity; using Avalonia.Svg.Skia; using Svg.Skia; using Xunit; @@ -9,6 +14,18 @@ namespace Avalonia.Svg.Skia.UnitTests; public class SvgControlTests { + private const string ButtonSvg = """ + + + + """; + + private const string InteractiveSvg = """ + + + + """; + [AvaloniaFact] public void AnimationPlaybackRate_NormalizesNonFiniteValues() { @@ -69,6 +86,143 @@ public void CurrentAnimationFrameCallback_AdvancesCurrentLoopState() Assert.False((bool)GetPrivateField(svg, "_animationRenderLoopRequested")); } + [AvaloniaFact] + public void SvgContent_BubblesPointerEventsToParentButton() + { + var pressed = 0; + var released = 0; + var svg = new Svg(new Uri("avares://Svg.Controls.Skia.Avalonia.UnitTests/")) + { + Source = ButtonSvg, + Width = 80, + Height = 80 + }; + var button = new Button + { + Content = svg + }; + button.AddHandler( + InputElement.PointerPressedEvent, + (_, _) => pressed++, + RoutingStrategies.Bubble, + handledEventsToo: true); + button.AddHandler( + InputElement.PointerReleasedEvent, + (_, _) => released++, + RoutingStrategies.Bubble, + handledEventsToo: true); + var window = new Window + { + Width = 160, + Height = 160, + Content = button + }; + + window.Show(); + try + { + window.MouseMove(new Point(80, 80), RawInputModifiers.None); + window.MouseDown(new Point(80, 80), MouseButton.Left, RawInputModifiers.None); + Assert.True(button.IsPressed); + window.MouseUp(new Point(80, 80), MouseButton.Left, RawInputModifiers.None); + + Assert.Equal(1, pressed); + Assert.Equal(1, released); + } + finally + { + window.Close(); + } + } + + [AvaloniaFact] + public void SvgInteractionCapture_ClearsWhenReleasedOutsideControl() + { + var svg = new Svg(new Uri("avares://Svg.Controls.Skia.Avalonia.UnitTests/")) + { + Source = InteractiveSvg, + Width = 80, + Height = 80 + }; + var canvas = new Canvas + { + Width = 220, + Height = 180, + Children = { svg } + }; + Canvas.SetLeft(svg, 20); + Canvas.SetTop(svg, 20); + var window = new Window + { + Width = 220, + Height = 180, + Content = canvas + }; + + window.Show(); + try + { + window.MouseMove(new Point(40, 40), RawInputModifiers.None); + window.MouseDown(new Point(40, 40), MouseButton.Left, RawInputModifiers.None); + Assert.NotNull(svg.Interaction.CapturedElement); + + window.MouseUp(new Point(180, 140), MouseButton.Left, RawInputModifiers.None); + + Assert.Null(svg.Interaction.CapturedElement); + } + finally + { + window.Close(); + } + } + + [AvaloniaFact] + public void SvgInteractionCapture_ClearsWhenPointerCaptureLost() + { + var svg = new CaptureLossSvg(new Uri("avares://Svg.Controls.Skia.Avalonia.UnitTests/")) + { + Source = InteractiveSvg, + Width = 80, + Height = 80 + }; + var window = new Window + { + Width = 160, + Height = 160, + Content = svg + }; + + window.Show(); + try + { + window.MouseMove(new Point(40, 40), RawInputModifiers.None); + window.MouseDown(new Point(40, 40), MouseButton.Left, RawInputModifiers.None); + Assert.NotNull(svg.Interaction.CapturedElement); + Assert.NotNull(svg.Interaction.PressedElement); + Assert.NotNull(svg.PressedPointer); + + svg.PressedPointer!.Capture(null); + + Assert.Null(svg.Interaction.CapturedElement); + Assert.Null(svg.Interaction.PressedElement); + } + finally + { + window.Close(); + } + } + + private sealed class CaptureLossSvg(Uri baseUri) : Svg(baseUri) + { + public IPointer? PressedPointer { get; private set; } + + protected override void OnPointerPressed(PointerPressedEventArgs e) + { + base.OnPointerPressed(e); + PressedPointer = e.Pointer; + } + } + private static void InvokeAnimationFrameCallback(Svg svg, long generation) { var callback = typeof(Svg).GetMethod(