From a7912ad51d3dfbcdc20da8ffd28c2dd549f77669 Mon Sep 17 00:00:00 2001 From: Stefan <203273530+sb-develop@users.noreply.github.com> Date: Sat, 19 Apr 2025 12:05:27 +0200 Subject: [PATCH 1/2] Views for WinForms --- .../Properties/AssemblyInfo.cs | 29 +++ .../RivePlayer.Properties.cs | 163 ++++++++++++ RiveSharp.Views.WinForms/RivePlayer.cs | 240 ++++++++++++++++++ .../RiveSharp.Views.WinForms.csproj | 28 ++ RiveSharp.Views.WinForms/StateMachineInput.cs | 170 +++++++++++++ .../StateMachineInputCollection.cs | 64 +++++ native/RiveSharpInterop.cpp | 4 +- 7 files changed, 696 insertions(+), 2 deletions(-) create mode 100644 RiveSharp.Views.WinForms/Properties/AssemblyInfo.cs create mode 100644 RiveSharp.Views.WinForms/RivePlayer.Properties.cs create mode 100644 RiveSharp.Views.WinForms/RivePlayer.cs create mode 100644 RiveSharp.Views.WinForms/RiveSharp.Views.WinForms.csproj create mode 100644 RiveSharp.Views.WinForms/StateMachineInput.cs create mode 100644 RiveSharp.Views.WinForms/StateMachineInputCollection.cs diff --git a/RiveSharp.Views.WinForms/Properties/AssemblyInfo.cs b/RiveSharp.Views.WinForms/Properties/AssemblyInfo.cs new file mode 100644 index 0000000..b8f7302 --- /dev/null +++ b/RiveSharp.Views.WinForms/Properties/AssemblyInfo.cs @@ -0,0 +1,29 @@ +using System.Reflection; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; + +// General Information about an assembly is controlled through the following +// set of attributes. Change these attribute values to modify the information +// associated with an assembly. +[assembly: AssemblyTitle("RiveSharp.Views.WinForms")] +[assembly: AssemblyDescription("")] +[assembly: AssemblyConfiguration("")] +[assembly: AssemblyCompany("Rive")] +[assembly: AssemblyProduct("RiveSharp.Views.WinForms")] +[assembly: AssemblyCopyright("Copyright © sb")] +[assembly: AssemblyTrademark("")] +[assembly: AssemblyCulture("")] + +// Version information for an assembly consists of the following four values: +// +// Major Version +// Minor Version +// Build Number +// Revision +// +// You can specify all the values or you can default the Build and Revision Numbers +// by using the '*' as shown below: +// [assembly: AssemblyVersion("1.0.*")] +[assembly: AssemblyVersion("1.0.0.0")] +[assembly: AssemblyFileVersion("1.0.0.0")] +[assembly: ComVisible(false)] diff --git a/RiveSharp.Views.WinForms/RivePlayer.Properties.cs b/RiveSharp.Views.WinForms/RivePlayer.Properties.cs new file mode 100644 index 0000000..6788205 --- /dev/null +++ b/RiveSharp.Views.WinForms/RivePlayer.Properties.cs @@ -0,0 +1,163 @@ +using RiveSharp; +using System; +using System.Collections.Generic; +using System.ComponentModel; +using System.Threading; + +namespace RiveSharp.Views +{ + public partial class RivePlayer + { + #region Events + + public event PropertyChangedEventHandler PropertyChanged; + + #endregion + + #region Fields + + private string _source; + private string _artboard; + private string _stateMachine; + private string _animation; + private StateMachineInputCollection _stateMachineInputs; + + #endregion + + #region Properties + + // Filename of the .riv file to open. Can be a file path or a URL. + public string Source + { + get => _source; + set + { + if (_source != value) + { + _source = value; + OnPropertyChanged(nameof(Source)); + OnSourceNameChanged(value); + } + } + } + + // Name of the artboard to load from the .riv file. If null or empty, the default artboard will be loaded. + public string Artboard + { + get => _artboard; + set + { + if (_artboard != value) + { + _artboard = value; + OnPropertyChanged(nameof(Artboard)); + OnArtboardNameChanged(value); + } + } + } + + // Name of the state machine to load from the .riv file. + public string StateMachine + { + get => _stateMachine; + set + { + if (_stateMachine != value) + { + _stateMachine = value; + OnPropertyChanged(nameof(StateMachine)); + OnStateMachineNameChanged(value); + } + } + } + + // Name of the fallback animation to load from the .riv if StateMachine is null or empty. + public string Animation + { + get => _animation; + set + { + if (_animation != value) + { + _animation = value; + OnPropertyChanged(nameof(Animation)); + OnAnimationNameChanged(value); + } + } + } + + public StateMachineInputCollection StateMachineInputs + { + get => _stateMachineInputs; + set + { + if (_stateMachineInputs != value) + { + _stateMachineInputs = value; + OnPropertyChanged(nameof(StateMachineInputs)); + } + } + } + + #endregion + + #region Methods + + private void OnSourceNameChanged(string newValue) + { + // Clear the current Scene while we wait for the new one to load. + sceneActionsQueue.Enqueue(() => _scene = new Scene()); + _activeSourceFileLoader?.Cancel(); + + _activeSourceFileLoader = new CancellationTokenSource(); + // Defer state machine inputs here until the new file is loaded. + _deferredSMInputsDuringFileLoad = new List(); + LoadSourceFileDataAsync(newValue, _activeSourceFileLoader.Token); + } + + private void OnArtboardNameChanged(string newValue) + { + sceneActionsQueue.Enqueue(() => _artboardName = newValue); + if (_activeSourceFileLoader != null) + { + // If a file is currently loading async, it will apply the new artboard once it completes. + _deferredSMInputsDuringFileLoad.Clear(); + } + else + { + sceneActionsQueue.Enqueue(() => UpdateScene(SceneUpdates.Artboard)); + } + } + + private void OnStateMachineNameChanged(string newValue) + { + sceneActionsQueue.Enqueue(() => _stateMachineName = newValue); + if (_activeSourceFileLoader != null) + { + // If a file is currently loading async, it will apply the new state machine once it completes. + _deferredSMInputsDuringFileLoad.Clear(); + } + else + { + sceneActionsQueue.Enqueue(() => UpdateScene(SceneUpdates.AnimationOrStateMachine)); + } + } + + private void OnAnimationNameChanged(string newValue) + { + sceneActionsQueue.Enqueue(() => _animationName = newValue); + // If a file is currently loading async, it will apply the new animation once it completes. + if (_activeSourceFileLoader == null) + { + sceneActionsQueue.Enqueue(() => UpdateScene(SceneUpdates.AnimationOrStateMachine)); + } + } + + private void OnPropertyChanged(string propertyName) + { + PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName)); + } + + #endregion + } +} diff --git a/RiveSharp.Views.WinForms/RivePlayer.cs b/RiveSharp.Views.WinForms/RivePlayer.cs new file mode 100644 index 0000000..13f61ca --- /dev/null +++ b/RiveSharp.Views.WinForms/RivePlayer.cs @@ -0,0 +1,240 @@ +using RiveSharp; +using SkiaSharp.Views.Desktop; +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.ComponentModel; +using System.IO; +using System.Net; +using System.Threading; +using System.Windows.Forms; + +namespace RiveSharp.Views +{ + // Implements a simple view that plays content from a .riv file. + [ToolboxItem(true), DesignTimeVisible(true)] + public partial class RivePlayer : SKControl, INotifyPropertyChanged + { + #region Delegates + + private delegate void PointerHandler(Vec2D pos); + + #endregion + + #region Nested Types: SceneUpdates + + private enum SceneUpdates + { + File = 3, + Artboard = 2, + AnimationOrStateMachine = 1, + }; + + #endregion + + #region Fields + + private CancellationTokenSource _activeSourceFileLoader = null; + private Scene _scene = new Scene(); + private readonly ConcurrentQueue sceneActionsQueue = new ConcurrentQueue(); + + private string _artboardName; + private string _animationName; + private string _stateMachineName; + DateTime? _lastPaintTime; + private List _deferredSMInputsDuringFileLoad = null; + private readonly System.Windows.Forms.Timer _timer = new System.Windows.Forms.Timer(); + + #endregion + + #region Constructor + + public RivePlayer() + { + SetStyle(ControlStyles.AllPaintingInWmPaint | ControlStyles.OptimizedDoubleBuffer | ControlStyles.ResizeRedraw, true); + + if (!DesignMode) + { + StateMachineInputs = new StateMachineInputCollection(this); + + // Initialize SKControl for rendering + PaintSurface += OnPaintSurface; + + _timer.Interval = 1000 / 60; // 60 fps + _timer.Tick += (s, e) => { if (Visible) { Invalidate(); } }; + + MouseDown += (s, e) => HandlePointerEvent(_scene.PointerDown, e); + MouseMove += (s, e) => HandlePointerEvent(_scene.PointerMove, e); + MouseUp += (s, e) => HandlePointerEvent(_scene.PointerUp, e); + } + } + + #endregion + + #region Methods + + /// + protected override void Dispose(bool disposing) + { + _timer.Stop(); + _timer.Dispose(); + + base.Dispose(disposing); + } + + /// + protected override void OnVisibleChanged(EventArgs e) + { + base.OnVisibleChanged(e); + + _timer.Enabled = Visible; + } + + private async void LoadSourceFileDataAsync(string name, CancellationToken cancellationToken) + { + byte[] data = null; + if (Uri.TryCreate(name, UriKind.Absolute, out var uri)) + { + using var client = new WebClient(); + data = await client.DownloadDataTaskAsync(uri); + } + else + { + var filePath = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, name); + if (File.Exists(filePath) && !cancellationToken.IsCancellationRequested) + { + data = await File.ReadAllBytesAsync(filePath, cancellationToken); + } + } + + if (data != null && !cancellationToken.IsCancellationRequested) + { + sceneActionsQueue.Enqueue(() => UpdateScene(SceneUpdates.File, data)); + foreach (Action stateMachineInput in _deferredSMInputsDuringFileLoad) + { + sceneActionsQueue.Enqueue(stateMachineInput); + } + } + _deferredSMInputsDuringFileLoad = null; + _activeSourceFileLoader = null; + } + + private void EnqueueStateMachineInput(Action stateMachineInput) + { + if (_deferredSMInputsDuringFileLoad != null) + { + _deferredSMInputsDuringFileLoad.Add(stateMachineInput); + } + else + { + sceneActionsQueue.Enqueue(stateMachineInput); + } + } + + public void SetBool(string name, bool value) + { + EnqueueStateMachineInput(() => _scene.SetBool(name, value)); + } + + public void SetNumber(string name, float value) + { + EnqueueStateMachineInput(() => _scene.SetNumber(name, value)); + } + + public void FireTrigger(string name) + { + EnqueueStateMachineInput(() => _scene.FireTrigger(name)); + } + + private void HandlePointerEvent(PointerHandler handler, MouseEventArgs e) + { + if (_activeSourceFileLoader != null) + { + return; + } + + var viewSize = ClientSize; + var pointerPos = e.Location; + + sceneActionsQueue.Enqueue(() => + { + Mat2D mat = ComputeAlignment(viewSize.Width, viewSize.Height); + if (mat.Invert(out var inverse)) + { + Vec2D artboardPos = inverse * new Vec2D(pointerPos.X, pointerPos.Y); + handler(artboardPos); + } + }); + } + + private void OnPaintSurface(object sender, SKPaintSurfaceEventArgs e) + { + while (sceneActionsQueue.TryDequeue(out var action)) + { + action(); + } + + if (!_scene.IsLoaded) + { + return; + } + + var now = DateTime.UtcNow; + if (_lastPaintTime != null) + { + _scene.AdvanceAndApply((now - _lastPaintTime.Value).TotalSeconds); + } + _lastPaintTime = now; + + e.Surface.Canvas.Clear(); + var renderer = new Renderer(e.Surface.Canvas); + renderer.Save(); + renderer.Transform(ComputeAlignment(e.Info.Width, e.Info.Height)); + _scene.Draw(renderer); + renderer.Restore(); + } + + private void UpdateScene(SceneUpdates updates, byte[] sourceFileData = null) + { + if (updates >= SceneUpdates.File) + { + _scene.LoadFile(sourceFileData); + } + if (updates >= SceneUpdates.Artboard) + { + _scene.LoadArtboard(_artboardName); + } + if (updates >= SceneUpdates.AnimationOrStateMachine) + { + if (!string.IsNullOrEmpty(_stateMachineName)) + { + _scene.LoadStateMachine(_stateMachineName); + } + else if (!string.IsNullOrEmpty(_animationName)) + { + _scene.LoadAnimation(_animationName); + } + else + { + if (!_scene.LoadStateMachine(null)) + { + _scene.LoadAnimation(null); + } + } + } + } + + private Mat2D ComputeAlignment(double width, double height) + { + return ComputeAlignment(new AABB(0, 0, (float)width, (float)height)); + } + + private Mat2D ComputeAlignment(AABB frame) + { + return Renderer.ComputeAlignment(Fit.Contain, Alignment.Center, frame, + new AABB(0, 0, _scene.Width, _scene.Height)); + } + + #endregion + } +} diff --git a/RiveSharp.Views.WinForms/RiveSharp.Views.WinForms.csproj b/RiveSharp.Views.WinForms/RiveSharp.Views.WinForms.csproj new file mode 100644 index 0000000..1c62129 --- /dev/null +++ b/RiveSharp.Views.WinForms/RiveSharp.Views.WinForms.csproj @@ -0,0 +1,28 @@ + + + + net8.0-windows + false + true + disable + enable + true + + + + true + Off + + + true + false + Off + + + + + + + + + diff --git a/RiveSharp.Views.WinForms/StateMachineInput.cs b/RiveSharp.Views.WinForms/StateMachineInput.cs new file mode 100644 index 0000000..dd7923f --- /dev/null +++ b/RiveSharp.Views.WinForms/StateMachineInput.cs @@ -0,0 +1,170 @@ +using System; +using System.ComponentModel; + +namespace RiveSharp.Views +{ + // This base class wraps a custom, named state machine input value. + public abstract class StateMachineInput : INotifyPropertyChanged + { + #region Fields + + private string _target; + private WeakReference _rivePlayer; + + #endregion + + #region Events + + public event PropertyChangedEventHandler PropertyChanged; + + #endregion + + #region Properties + + public string Target + { + get => _target; // Must be null-checked before use. + set + { + if (_target != value) + { + _target = value; + OnPropertyChanged(nameof(Target)); + Apply(); + } + } + } + + protected WeakReference RivePlayer => _rivePlayer; + + #endregion + + #region Methods + + // Sets _rivePlayer to the given rivePlayer object and applies our input value to the state machine. + // Does nothing if _rivePlayer was already equal to rivePlayer. + internal void SetRivePlayer(WeakReference rivePlayer) + { + _rivePlayer = rivePlayer; + Apply(); + } + + protected void Apply() + { + if (!string.IsNullOrEmpty(_target) && _rivePlayer is not null && _rivePlayer.TryGetTarget(out var rivePlayer)) + { + Apply(rivePlayer, _target); + } + } + + // Applies our input value to the rivePlayer's state machine. + // rivePlayer and inputName are guaranteed to not be null or empty. + protected abstract void Apply(RivePlayer rivePlayer, string inputName); + + protected void OnPropertyChanged(string propertyName) + { + PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName)); + } + + #endregion + } + + public class BoolInput : StateMachineInput + { + #region Fields + + private bool _value; + + #endregion + + #region Properties + + public bool Value + { + get => _value; + set + { + if (_value != value) + { + _value = value; + OnPropertyChanged(nameof(Value)); + Apply(); + } + } + } + + #endregion + + #region Methods + + /// + protected override void Apply(RivePlayer rivePlayer, string inputName) + { + rivePlayer.SetBool(inputName, Value); + } + + #endregion + } + + public class NumberInput : StateMachineInput + { + #region Fields + + private double _value; + + #endregion + + #region Properties + + public double Value + { + get => _value; + set + { + if (_value != value) + { + _value = value; + OnPropertyChanged(nameof(Value)); + Apply(); + } + } + } + + #endregion + + #region Methods + + /// + protected override void Apply(RivePlayer rivePlayer, string inputName) + { + rivePlayer.SetNumber(inputName, (float)Value); + } + + #endregion + } + + public class TriggerInput : StateMachineInput + { + #region Methods + + public void Fire() + { + if (!string.IsNullOrEmpty(Target) && RivePlayer is not null && RivePlayer.TryGetTarget(out var rivePlayer)) + { + rivePlayer.FireTrigger(Target); + } + } + + // Make a Fire() overload that matches the EventHandler delegate. + // This allows us to bind it to events like Button.Click in WinForms. + public void Fire(object sender, EventArgs e) + { + Fire(); + } + + /// + protected override void Apply(RivePlayer rivePlayer, string inputName) { } + + #endregion + } +} diff --git a/RiveSharp.Views.WinForms/StateMachineInputCollection.cs b/RiveSharp.Views.WinForms/StateMachineInputCollection.cs new file mode 100644 index 0000000..6d315c2 --- /dev/null +++ b/RiveSharp.Views.WinForms/StateMachineInputCollection.cs @@ -0,0 +1,64 @@ +using System; +using System.Collections.ObjectModel; +using System.Collections.Specialized; + +namespace RiveSharp.Views +{ + // Manages a collection of StateMachineInput objects for RivePlayer. + public class StateMachineInputCollection : ObservableCollection + { + #region Fields + + private readonly WeakReference _rivePlayer; + + #endregion + + #region Constructor + + public StateMachineInputCollection(RivePlayer rivePlayer) + { + _rivePlayer = new WeakReference(rivePlayer); + CollectionChanged += InputsVectorChanged; + } + + #endregion + + #region Methods + + private void InputsVectorChanged(object sender, NotifyCollectionChangedEventArgs e) + { + switch (e.Action) + { + case NotifyCollectionChangedAction.Add: + case NotifyCollectionChangedAction.Replace: + if (e.NewItems != null) + { + foreach (StateMachineInput input in e.NewItems) + { + input.SetRivePlayer(_rivePlayer); + } + } + break; + + case NotifyCollectionChangedAction.Remove: + if (e.OldItems != null) + { + foreach (StateMachineInput input in e.OldItems) + { + input.SetRivePlayer(null); + } + } + break; + + case NotifyCollectionChangedAction.Reset: + foreach (StateMachineInput input in this) + { + input.SetRivePlayer(null); + } + break; + } + } + + #endregion + } +} diff --git a/native/RiveSharpInterop.cpp b/native/RiveSharpInterop.cpp index b8bfc7f..50830af 100644 --- a/native/RiveSharpInterop.cpp +++ b/native/RiveSharpInterop.cpp @@ -779,7 +779,7 @@ RIVE_DLL_INT32 Scene_Loop(intptr_t ref) { if (Scene* scene = reinterpret_cast(ref)->scene()) { - return (int)reinterpret_cast(ref)->scene()->loop(); + return (int)scene->loop(); } return 0; } @@ -788,7 +788,7 @@ RIVE_DLL_INT8_BOOL Scene_IsTranslucent(intptr_t ref) { if (Scene* scene = reinterpret_cast(ref)->scene()) { - return reinterpret_cast(ref)->scene()->isTranslucent(); + return scene->isTranslucent(); } return 0; } From 78ccc6ed14ed944c6c4e86fbfdd13f552e1b20d8 Mon Sep 17 00:00:00 2001 From: Stefan <203273530+sb-develop@users.noreply.github.com> Date: Sat, 19 Apr 2025 16:00:02 +0200 Subject: [PATCH 2/2] add Views.WinForms project to sln --- RiveSharpSample.sln | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/RiveSharpSample.sln b/RiveSharpSample.sln index 7f9a7af..4b08675 100644 --- a/RiveSharpSample.sln +++ b/RiveSharpSample.sln @@ -20,6 +20,8 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution .editorconfig = .editorconfig EndProjectSection EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "RiveSharp.Views.WinForms", "RiveSharp.Views.WinForms\RiveSharp.Views.WinForms.csproj", "{CD36E13E-EDA4-1722-D50E-E2F6789CA34B}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -174,6 +176,26 @@ Global {FB609D7C-E797-9E0D-9084-84107C9A1A0F}.Release|x64.Build.0 = Release|x64 {FB609D7C-E797-9E0D-9084-84107C9A1A0F}.Release|x86.ActiveCfg = Release|Win32 {FB609D7C-E797-9E0D-9084-84107C9A1A0F}.Release|x86.Build.0 = Release|Win32 + {CD36E13E-EDA4-1722-D50E-E2F6789CA34B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {CD36E13E-EDA4-1722-D50E-E2F6789CA34B}.Debug|Any CPU.Build.0 = Debug|Any CPU + {CD36E13E-EDA4-1722-D50E-E2F6789CA34B}.Debug|ARM.ActiveCfg = Debug|Any CPU + {CD36E13E-EDA4-1722-D50E-E2F6789CA34B}.Debug|ARM.Build.0 = Debug|Any CPU + {CD36E13E-EDA4-1722-D50E-E2F6789CA34B}.Debug|ARM64.ActiveCfg = Debug|Any CPU + {CD36E13E-EDA4-1722-D50E-E2F6789CA34B}.Debug|ARM64.Build.0 = Debug|Any CPU + {CD36E13E-EDA4-1722-D50E-E2F6789CA34B}.Debug|x64.ActiveCfg = Debug|Any CPU + {CD36E13E-EDA4-1722-D50E-E2F6789CA34B}.Debug|x64.Build.0 = Debug|Any CPU + {CD36E13E-EDA4-1722-D50E-E2F6789CA34B}.Debug|x86.ActiveCfg = Debug|Any CPU + {CD36E13E-EDA4-1722-D50E-E2F6789CA34B}.Debug|x86.Build.0 = Debug|Any CPU + {CD36E13E-EDA4-1722-D50E-E2F6789CA34B}.Release|Any CPU.ActiveCfg = Release|Any CPU + {CD36E13E-EDA4-1722-D50E-E2F6789CA34B}.Release|Any CPU.Build.0 = Release|Any CPU + {CD36E13E-EDA4-1722-D50E-E2F6789CA34B}.Release|ARM.ActiveCfg = Release|Any CPU + {CD36E13E-EDA4-1722-D50E-E2F6789CA34B}.Release|ARM.Build.0 = Release|Any CPU + {CD36E13E-EDA4-1722-D50E-E2F6789CA34B}.Release|ARM64.ActiveCfg = Release|Any CPU + {CD36E13E-EDA4-1722-D50E-E2F6789CA34B}.Release|ARM64.Build.0 = Release|Any CPU + {CD36E13E-EDA4-1722-D50E-E2F6789CA34B}.Release|x64.ActiveCfg = Release|Any CPU + {CD36E13E-EDA4-1722-D50E-E2F6789CA34B}.Release|x64.Build.0 = Release|Any CPU + {CD36E13E-EDA4-1722-D50E-E2F6789CA34B}.Release|x86.ActiveCfg = Release|Any CPU + {CD36E13E-EDA4-1722-D50E-E2F6789CA34B}.Release|x86.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE