From 338d739bf701ff3a444f3ee7310cdae2120e2c3f Mon Sep 17 00:00:00 2001
From: Luigi Rosso <luigi.rosso@gmail.com>
Date: Wed, 11 May 2022 15:23:39 -0700
Subject: [PATCH] Add a Listener to the RiveAnimation widget when a
 StateMachineController has events.

---
 example/lib/main.dart                  | 15 +++++
 example/lib/simple_machine_action.dart | 32 ++++++++++
 lib/src/rive.dart                      |  1 +
 lib/src/rive_core/artboard.dart        |  4 ++
 lib/src/rive_file.dart                 |  5 +-
 lib/src/rive_render_box.dart           | 58 ++++++++++--------
 lib/src/widgets/rive_animation.dart    | 85 ++++++++++++++++++++++++--
 7 files changed, 168 insertions(+), 32 deletions(-)
 create mode 100644 example/lib/simple_machine_action.dart

diff --git a/example/lib/main.dart b/example/lib/main.dart
index eeb8dd9..057570c 100644
--- a/example/lib/main.dart
+++ b/example/lib/main.dart
@@ -6,6 +6,7 @@ import 'package:rive_example/little_machine.dart';
 import 'package:rive_example/play_one_shot_animation.dart';
 import 'package:rive_example/play_pause_animation.dart';
 import 'package:rive_example/simple_animation.dart';
+import 'package:rive_example/simple_machine_action.dart';
 import 'package:rive_example/simple_state_machine.dart';
 import 'package:rive_example/state_machine_skills.dart';
 
@@ -148,6 +149,20 @@ class Home extends StatelessWidget {
                 );
               },
             ),
+            const SizedBox(
+              height: 10,
+            ),
+            ElevatedButton(
+              child: const Text('State Machine with Action'),
+              onPressed: () {
+                Navigator.push(
+                  context,
+                  MaterialPageRoute<void>(
+                    builder: (context) => const StateMachineAction(),
+                  ),
+                );
+              },
+            ),
           ],
         ),
       ),
diff --git a/example/lib/simple_machine_action.dart b/example/lib/simple_machine_action.dart
new file mode 100644
index 0000000..aebd943
--- /dev/null
+++ b/example/lib/simple_machine_action.dart
@@ -0,0 +1,32 @@
+import 'package:flutter/material.dart';
+import 'package:rive/rive.dart';
+
+class StateMachineAction extends StatefulWidget {
+  const StateMachineAction({Key? key}) : super(key: key);
+
+  @override
+  _StateMachineActionState createState() => _StateMachineActionState();
+}
+
+class _StateMachineActionState extends State<StateMachineAction> {
+  void _onRiveInit(Artboard artboard) {
+    final controller = StateMachineController.fromArtboard(artboard, 'Switch');
+    artboard.addController(controller!);
+  }
+
+  @override
+  Widget build(BuildContext context) {
+    return Scaffold(
+      appBar: AppBar(
+        title: const Text('Light Switch'),
+      ),
+      body: Center(
+        child: RiveAnimation.asset(
+          'assets/light_switch.riv',
+          fit: BoxFit.contain,
+          onInit: _onRiveInit,
+        ),
+      ),
+    );
+  }
+}
diff --git a/lib/src/rive.dart b/lib/src/rive.dart
index 821ddf4..03aed12 100644
--- a/lib/src/rive.dart
+++ b/lib/src/rive.dart
@@ -112,6 +112,7 @@ class RiveRenderObject extends RiveRenderBox {
 
   @override
   void draw(Canvas canvas, Mat2D viewTransform) {
+    canvas.transform(viewTransform.mat4);
     artboard.draw(canvas);
   }
 }
diff --git a/lib/src/rive_core/artboard.dart b/lib/src/rive_core/artboard.dart
index 2aea45d..5c89140 100644
--- a/lib/src/rive_core/artboard.dart
+++ b/lib/src/rive_core/artboard.dart
@@ -360,6 +360,10 @@ class Artboard extends ArtboardBase with ShapePaintContainer {
   /// advances.
   final Set<RiveAnimationController> _animationControllers = {};
 
+  /// Access a read-only iterator of currently applied animation controllers.
+  Iterable<RiveAnimationController> get animationControllers =>
+      _animationControllers;
+
   /// Add an animation controller to this artboard. Playing will be scheduled if
   /// it's already playing.
   bool addController(RiveAnimationController controller) {
diff --git a/lib/src/rive_file.dart b/lib/src/rive_file.dart
index 0853f8e..fb85bb7 100644
--- a/lib/src/rive_file.dart
+++ b/lib/src/rive_file.dart
@@ -20,6 +20,7 @@ import 'package:rive/src/rive_core/animation/keyed_property.dart';
 import 'package:rive/src/rive_core/animation/layer_state.dart';
 import 'package:rive/src/rive_core/animation/linear_animation.dart';
 import 'package:rive/src/rive_core/animation/state_machine.dart';
+import 'package:rive/src/rive_core/animation/state_machine_event.dart';
 import 'package:rive/src/rive_core/animation/state_machine_layer.dart';
 import 'package:rive/src/rive_core/animation/state_transition.dart';
 import 'package:rive/src/rive_core/artboard.dart';
@@ -158,7 +159,9 @@ class RiveFile {
         case StateMachineLayerBase.typeKey:
           stackObject = StateMachineLayerImporter(object as StateMachineLayer);
           break;
-
+        case StateMachineEventBase.typeKey:
+          stackObject = StateMachineEventImporter(object as StateMachineEvent);
+          break;
         case EntryStateBase.typeKey:
         case AnyStateBase.typeKey:
         case ExitStateBase.typeKey:
diff --git a/lib/src/rive_render_box.dart b/lib/src/rive_render_box.dart
index 1cd3ebe..7fe40e0 100644
--- a/lib/src/rive_render_box.dart
+++ b/lib/src/rive_render_box.dart
@@ -194,30 +194,25 @@ abstract class RiveRenderBox extends RenderBox {
   /// be called after advancing. Return true to prevent regular paint.
   bool customPaint(PaintingContext context, Offset offset) => false;
 
-  @protected
-  @override
-  void paint(PaintingContext context, Offset offset) {
-    _frameCallbackId = -1;
-    if (advance(_elapsedSeconds)) {
-      scheduleRepaint();
-    } else {
-      _stopwatch.stop();
+  Vec2D globalToArtboard(Offset globalPosition) {
+    var local = globalToLocal(globalPosition);
+    var alignArtboard = computeAlignment();
+    var localToArtboard = Mat2D();
+    var localAsVec = Vec2D.fromValues(local.dx, local.dy);
+    if (!Mat2D.invert(localToArtboard, alignArtboard)) {
+      return localAsVec;
     }
-    _elapsedSeconds = 0;
-
-    if (customPaint(context, offset)) {
-      return;
-    }
-
-    final Canvas canvas = context.canvas;
+    return Vec2D.transformMat2D(Vec2D(), localAsVec, localToArtboard);
+  }
 
+  Mat2D computeAlignment([Offset offset = Offset.zero]) {
     AABB bounds = aabb;
 
     double contentWidth = bounds[2] - bounds[0];
     double contentHeight = bounds[3] - bounds[1];
 
     if (contentWidth == 0 || contentHeight == 0) {
-      return;
+      return Mat2D();
     }
 
     double x = -1 * bounds[0] -
@@ -229,9 +224,6 @@ abstract class RiveRenderBox extends RenderBox {
 
     double scaleX = 1.0, scaleY = 1.0;
 
-    canvas.save();
-    beforeDraw(canvas, offset);
-
     switch (_fit) {
       case BoxFit.fill:
         scaleX = size.width / contentWidth;
@@ -278,14 +270,30 @@ abstract class RiveRenderBox extends RenderBox {
     center[4] = x;
     center[5] = y;
     Mat2D.multiply(transform, transform, center);
+    return transform;
+  }
 
-    canvas.translate(
-      offset.dx + size.width / 2.0 + (_alignment.x * size.width / 2.0),
-      offset.dy + size.height / 2.0 + (_alignment.y * size.height / 2.0),
-    );
+  @protected
+  @override
+  void paint(PaintingContext context, Offset offset) {
+    _frameCallbackId = -1;
+    if (advance(_elapsedSeconds)) {
+      scheduleRepaint();
+    } else {
+      _stopwatch.stop();
+    }
+    _elapsedSeconds = 0;
 
-    canvas.scale(scaleX, scaleY);
-    canvas.translate(x, y);
+    if (customPaint(context, offset)) {
+      return;
+    }
+
+    final Canvas canvas = context.canvas;
+
+    canvas.save();
+    beforeDraw(canvas, offset);
+
+    var transform = computeAlignment(offset);
 
     draw(canvas, transform);
 
diff --git a/lib/src/widgets/rive_animation.dart b/lib/src/widgets/rive_animation.dart
index 8f73d8f..c945a41 100644
--- a/lib/src/widgets/rive_animation.dart
+++ b/lib/src/widgets/rive_animation.dart
@@ -1,5 +1,6 @@
 import 'package:flutter/widgets.dart';
 import 'package:rive/rive.dart';
+import 'package:rive/src/rive_core/math/vec2d.dart';
 
 /// Specifies whether a source is from an asset bundle or http
 enum _Source {
@@ -137,7 +138,8 @@ class _RiveAnimationState extends State<RiveAnimation> {
     // controller specified, select a default animation
     final animationNames = widget.animations.isEmpty &&
             widget.stateMachines.isEmpty &&
-            widget.controllers.isEmpty
+            widget.controllers.isEmpty &&
+            widget.onInit == null
         ? [artboard.animations.first.name]
         : widget.animations;
 
@@ -169,13 +171,84 @@ class _RiveAnimationState extends State<RiveAnimation> {
     super.dispose();
   }
 
+  Vec2D? _toArtboard(Offset local) {
+    RiveRenderObject? riveRenderer;
+    var renderObject = context.findRenderObject();
+    if (renderObject is! RenderBox) {
+      return null;
+    }
+    renderObject.visitChildren(
+      (child) {
+        if (child is RiveRenderObject) {
+          riveRenderer = child;
+        }
+      },
+    );
+    if (riveRenderer == null) {
+      return null;
+    }
+    var globalCoordinates = renderObject.localToGlobal(local);
+
+    return riveRenderer!.globalToArtboard(globalCoordinates);
+  }
+
+  Widget _optionalHitTester(BuildContext context, Widget child) {
+    assert(_artboard != null);
+    var hasHitTesting = _artboard!.animationControllers.any((controller) =>
+        controller is StateMachineController &&
+        controller.hitShapes.isNotEmpty);
+
+    if (hasHitTesting) {
+      void hitHelper(PointerEvent event,
+          void Function(StateMachineController, Vec2D) callback) {
+        var artboardPosition = _toArtboard(event.localPosition);
+        if (artboardPosition != null) {
+          var stateMachineControllers = _artboard!.animationControllers
+              .whereType<StateMachineController>();
+          for (final stateMachineController in stateMachineControllers) {
+            callback(stateMachineController, artboardPosition);
+          }
+        }
+      }
+
+      return Listener(
+        onPointerDown: (details) => hitHelper(
+          details,
+          (controller, artboardPosition) =>
+              controller.pointerDown(artboardPosition),
+        ),
+        onPointerUp: (details) => hitHelper(
+          details,
+          (controller, artboardPosition) =>
+              controller.pointerUp(artboardPosition),
+        ),
+        onPointerHover: (details) => hitHelper(
+          details,
+          (controller, artboardPosition) =>
+              controller.pointerMove(artboardPosition),
+        ),
+        onPointerMove: (details) => hitHelper(
+          details,
+          (controller, artboardPosition) =>
+              controller.pointerMove(artboardPosition),
+        ),
+        child: child,
+      );
+    }
+
+    return child;
+  }
+
   @override
   Widget build(BuildContext context) => _artboard != null
-      ? Rive(
-          artboard: _artboard!,
-          fit: widget.fit,
-          alignment: widget.alignment,
-          antialiasing: widget.antialiasing,
+      ? _optionalHitTester(
+          context,
+          Rive(
+            artboard: _artboard!,
+            fit: widget.fit,
+            alignment: widget.alignment,
+            antialiasing: widget.antialiasing,
+          ),
         )
       : widget.placeHolder ?? const SizedBox();
 }