From 1b415e7e2636439f27407614983edbed447b4e4b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Lima?= Date: Thu, 23 Nov 2023 19:07:49 +0000 Subject: [PATCH] feat: find widgets by click position --- uni/lib/main.dart | 16 +- .../find_element.dart | 67 +++++++ .../plausible_click_listener.dart | 176 ++++++++++++++++++ 3 files changed, 258 insertions(+), 1 deletion(-) create mode 100644 uni/lib/view/plausible_click_listener/find_element.dart create mode 100644 uni/lib/view/plausible_click_listener/plausible_click_listener.dart diff --git a/uni/lib/main.dart b/uni/lib/main.dart index 7bd8bb47f..216539741 100644 --- a/uni/lib/main.dart +++ b/uni/lib/main.dart @@ -41,6 +41,7 @@ import 'package:uni/view/locale_notifier.dart'; import 'package:uni/view/locations/locations.dart'; import 'package:uni/view/login/login.dart'; import 'package:uni/view/navigation_service.dart'; +import 'package:uni/view/plausible_click_listener/plausible_click_listener.dart'; import 'package:uni/view/restaurant/restaurant_page_view.dart'; import 'package:uni/view/schedule/schedule.dart'; import 'package:uni/view/theme.dart'; @@ -106,6 +107,8 @@ Future main() async { ? Plausible(plausibleUrl, plausibleDomain) : null; + plausible?.enabled = false; + if (plausible == null) { Logger().w('Plausible is not enabled'); } @@ -207,7 +210,8 @@ class ApplicationState extends State { SystemChrome.setPreferredOrientations([ DeviceOrientation.portraitUp, ]); - return Consumer2( + + final app = Consumer2( builder: (context, themeNotifier, localeNotifier, _) => MaterialApp( title: 'uni', navigatorKey: Application.navigatorKey, @@ -295,5 +299,15 @@ class ApplicationState extends State { }, ), ); + + final plausible = widget.plausible; + if (plausible == null) { + return app; + } + + return PlausibleClickListener( + plausible: plausible, + child: app, + ); } } diff --git a/uni/lib/view/plausible_click_listener/find_element.dart b/uni/lib/view/plausible_click_listener/find_element.dart new file mode 100644 index 000000000..97902581b --- /dev/null +++ b/uni/lib/view/plausible_click_listener/find_element.dart @@ -0,0 +1,67 @@ +import 'dart:collection'; + +import 'package:flutter/gestures.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/rendering.dart'; + +Element? _searchElementTreeUntil(Element root, bool Function(Element) predicate) { + final elementsToSearch = Queue()..add(root); + + while (elementsToSearch.isNotEmpty) { + final currentElement = elementsToSearch.removeFirst(); + if (predicate.call(currentElement)) { + return currentElement; + } + + currentElement.visitChildElements(elementsToSearch.add); + } + + return null; +} + +Element? _findClickedElement(Element root, HitTestResult hitTestResult) { + final searchOrder = hitTestResult.path + .map((e) => e.target) + .whereType() + .toList(); + + var currentElement = root; + while (searchOrder.isNotEmpty) { + final currentRenderObject = searchOrder.removeLast(); + final currentRenderObjectElement = _searchElementTreeUntil(currentElement, (element) { + return element is RenderObjectElement && element.renderObject == currentRenderObject; + }); + + if (currentRenderObjectElement == null) { + return null; + } + + currentElement = currentRenderObjectElement; + } + + return currentElement; +} + +Element? findElementAt(Element root, Offset position) { + final contextObject = root.findRenderObject(); + if (contextObject is! RenderBox) { + return null; + } + + final hits = BoxHitTestResult(); + final hasHit = contextObject.hitTest(hits, position: position); + + if (!hasHit) { + return null; + } + + var element = _findClickedElement(root, hits); + if (element == null) { + return null; + } + + element.visitAncestorElements((ancestor) { + print(ancestor); + return true; + }); +} \ No newline at end of file diff --git a/uni/lib/view/plausible_click_listener/plausible_click_listener.dart b/uni/lib/view/plausible_click_listener/plausible_click_listener.dart new file mode 100644 index 000000000..12ad28856 --- /dev/null +++ b/uni/lib/view/plausible_click_listener/plausible_click_listener.dart @@ -0,0 +1,176 @@ +import 'dart:ui'; + +import 'package:flutter/gestures.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/rendering.dart'; +import 'package:path/path.dart'; +import 'package:plausible_analytics/plausible_analytics.dart'; +import 'package:uni/view/plausible_click_listener/find_element.dart'; + +class PlausibleClickListener extends StatelessWidget { + + const PlausibleClickListener({required this.child, required this.plausible, super.key}); + + final Plausible plausible; + final Widget child; + + // Element? findElementAt(BuildContext context, Offset position) { + // final contextObject = context.findRenderObject(); + // if (contextObject is! RenderBox) { + // print("RIP 2"); + // return null; + // } + + // final hits = BoxHitTestResult(); + // final hasHit = contextObject.hitTest(hits, position: position); + + // if (!hasHit) { + // return null; + // } + + // T? tryCast(dynamic value) { + // return value is T ? value : null; + // } + + // final renderObjects = hits.path + // .map((e) => e.target) + // .whereType() + // // .nonNulls + // .toList(); + + // if (renderObjects.isEmpty) { + // return null; + // } + + // // Code inspired by https://github.com/flutter/flutter/blob/be3a4b37b3e9ab4e80d45b59bed53708b96d211f/packages/flutter/lib/src/widgets/framework.dart#L3005-L3021 + // RenderObjectElement? descendUntilRenderObjectElement(BuildContext parent) { + + // Element? getSingleChildOf(BuildContext context) { + // Element? result; + // context.visitChildElements((child) { + // assert(result == null, 'element has a single child'); + // result = child; + // }); + + // return result; + // } + + // BuildContext? result = parent; + // while (result is! RenderObjectElement?) { + // print(result); + // result = getSingleChildOf(result); + // } + + // print(result); + // return result; + // } + // print(renderObjects.reversed.toList()); + + // var currentElement = descendUntilRenderObjectElement(context); + // if (currentElement?.renderObject != renderObjects.removeLast()) { + // return null; + // } + + // print("\nINITIAL"); + + // while (currentElement != null && renderObjects.isNotEmpty) { + // print(""); + // print(""); + // print(""); + // print('On $currentElement'); + // print("Next two objects: ${renderObjects.reversed.take(2)}"); + // RenderObjectElement? nextElement; + // currentElement.visitChildElements((element) { + // print("Searching in $element"); + // if (nextElement != null) { + // print("Ignoring..."); + // return; + // } + // final descendedElement = descendUntilRenderObjectElement(element); + // print("Using $descendedElement with ${descendedElement?.renderObject}. Looking for ${renderObjects.last}"); + // print(descendedElement?.mounted); + // if (descendedElement?.renderObject == renderObjects.last) { + // nextElement = descendedElement; + // print("Found!"); + // } + // }); + + // currentElement = nextElement; + // renderObjects.removeLast(); + // } + + // print("\nRESULT"); + // print(currentElement); + // print(renderObjects); + + // // for (final renderObject in renderObjects) { + + // // RenderObjectElement? selectedChild; + // // currentElement.visitChildElements((element) { + + // // }) + // // currentElement = descendUntilRenderObjectElement(context); + // // } + + // print(renderObjects); + + + + // return null; + // } + + @override + Widget build(BuildContext context) { + print(context.runtimeType); + return Listener( + behavior: HitTestBehavior.opaque, + onPointerDown: (event) { + if (!context.mounted) { + return; + } + + + findElementAt(context as Element, event.position); + + // event. + // context.(recursiveTraversal('')); + /// TODO send: + /// if success: + /// - name + /// - x, y + /// if not sucess: + /// - x, y + + // plausible.event( + // name: 'pointerdown', + // props: { + // 'payload': { + // 'x': event.position.dx.toString(), + // 'y': event.position.dy.toString(), + // 'localX': event.localPosition.dx.toString(), + // 'localY': event.localPosition.dy.toString(), + // 'timestamp': event.timeStamp.toString(), + // 'pointer': event.pointer.toString(), + // }.toString(), + // } + // ); + }, + onPointerUp: (event) { + // plausible.event( + // name: 'pointerup', + // props: { + // 'payload': { + // 'x': event.position.dx.toString(), + // 'y': event.position.dy.toString(), + // 'localX': event.localPosition.dx.toString(), + // 'localY': event.localPosition.dy.toString(), + // 'timestamp': event.timeStamp.toString(), + // 'pointer': event.pointer.toString(), + // }.toString(), + // } + // ); + }, + child: child, + ); + } +}