Improve cursor detection

Signed-off-by: Hans Kokx <hans.d.kokx@gmail.com>
This commit is contained in:
2026-08-16 20:55:21 +02:00
parent 4770b29d98
commit 789daa7b48
+53 -34
View File
@@ -12,24 +12,27 @@ const winampChannel = MethodChannel("winamp");
/// name, so hover regions can switch to them. Best-effort: failures are
/// ignored because the native side may not be ready yet.
Future<void> registerSkinCursors(ClassicSkin skin) async {
for (final entry in skin.cursors.entries) {
final png = await _cursorPng(entry.value.image);
try {
await winampChannel.invokeMethod(
"registerCursor",
{
"name": entry.key,
"png": png,
"hotspotX": entry.value.hotspotX,
"hotspotY": entry.value.hotspotY,
},
);
} on PlatformException {
// ignore: best-effort
} on MissingPluginException {
// ignore: best-effort
}
}
// IMPROVEMENT: Load and register all cursors in parallel instead of sequentially.
await Future.wait(
skin.cursors.entries.map((entry) async {
final png = await _cursorPng(entry.value.image);
try {
await winampChannel.invokeMethod(
"registerCursor",
{
"name": entry.key,
"png": png,
"hotspotX": entry.value.hotspotX,
"hotspotY": entry.value.hotspotY,
},
);
} on PlatformException {
// ignore: best-effort
} on MissingPluginException {
// ignore: best-effort
}
}),
);
}
Future<Uint8List> _cursorPng(ui.Image image) async {
@@ -62,10 +65,9 @@ class SkinCursorsGate extends StatefulWidget {
}
class _SkinCursorsGateState extends State<SkinCursorsGate> {
/// Serializes cursor swaps so a slow native call from a stale enter/exit
/// event (e.g. during a drag when the window moves under the cursor) can
/// never leave the wrong cursor stuck after the latest request.
int _cursorSeq = 0;
// Track the current cursor locally to prevent flooding the platform channel
String _activeCursorName = "arrow";
bool _isHovering = false;
@override
void initState() {
@@ -78,25 +80,42 @@ class _SkinCursorsGateState extends State<SkinCursorsGate> {
super.didUpdateWidget(oldWidget);
if (oldWidget.skin != widget.skin) {
unawaited(registerSkinCursors(widget.skin));
// If the skin changes while the mouse is already inside the widget,
// we need to re-evaluate what cursor should be showing.
if (_isHovering) {
_updateCursor();
}
}
}
void _updateCursor() {
final hasCustom = widget.skin.cursors.containsKey("normal");
_setCursor(hasCustom ? "normal" : "arrow");
}
void _setCursor(String name) {
if (_activeCursorName == name) return;
_activeCursorName = name;
unawaited(setSkinCursor(name));
}
@override
Widget build(BuildContext context) {
final hasCustom = widget.skin.cursors.containsKey("normal");
return MouseRegion(
onEnter: (_) => _setCursor(hasCustom ? "normal" : "arrow"),
onExit: (_) => _setCursor("arrow"),
hitTestBehavior: HitTestBehavior.translucent,
// onHover is intentionally omitted. We only need to know when the
// mouse enters or exits the boundary to trigger the cursor change.
onEnter: (_) {
_isHovering = true;
_updateCursor();
},
onExit: (_) {
_isHovering = false;
_setCursor("arrow");
},
child: widget.child,
);
}
Future<void> _setCursor(String name) async {
final seq = ++_cursorSeq;
await setSkinCursor(name);
// A newer enter/exit won the race; leave that cursor in place.
if (seq != _cursorSeq) {
return;
}
}
}