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(