Move cusor handler functions into an abstract class
Signed-off-by: Hans Kokx <hans.d.kokx@gmail.com>
This commit is contained in:
+12
-12
@@ -68,16 +68,16 @@ class _CursorGridScreenState extends State<CursorGridScreen> {
|
||||
}
|
||||
|
||||
Future<void> _init() async {
|
||||
_systemScale = await systemCursorScale();
|
||||
_systemScale = await CursorHandler.systemCursorScale();
|
||||
unawaited(
|
||||
observeSystemCursorScale((scale) {
|
||||
CursorHandler.observeSystemCursorScale((scale) {
|
||||
if (!mounted) return;
|
||||
setState(() => _systemScale = scale);
|
||||
unawaited(_redecode());
|
||||
}),
|
||||
);
|
||||
unawaited(
|
||||
observeSystemDirection((_) {
|
||||
CursorHandler.observeSystemDirection((_) {
|
||||
if (!mounted) return;
|
||||
setState(() {});
|
||||
}),
|
||||
@@ -104,7 +104,7 @@ class _CursorGridScreenState extends State<CursorGridScreen> {
|
||||
_cursors = loaded;
|
||||
_loading = false;
|
||||
});
|
||||
unawaited(registerCursors(_cursors));
|
||||
unawaited(CursorHandler.registerCursors(_cursors));
|
||||
} catch (e) {
|
||||
if (!mounted) return;
|
||||
setState(() {
|
||||
@@ -143,13 +143,13 @@ class _CursorGridScreenState extends State<CursorGridScreen> {
|
||||
..addAll(raw);
|
||||
_dragging = false;
|
||||
});
|
||||
unawaited(registerCursors(_cursors));
|
||||
unawaited(CursorHandler.registerCursors(_cursors));
|
||||
}
|
||||
|
||||
/// Re-decodes any stored raw files with the current options, then re-registers.
|
||||
Future<void> _redecode() async {
|
||||
if (_rawFiles.isEmpty) {
|
||||
unawaited(registerCursors(_cursors));
|
||||
unawaited(CursorHandler.registerCursors(_cursors));
|
||||
return;
|
||||
}
|
||||
final loaded = <String, CursorAsset>{};
|
||||
@@ -165,7 +165,7 @@ class _CursorGridScreenState extends State<CursorGridScreen> {
|
||||
}
|
||||
if (!mounted) return;
|
||||
setState(() => _cursors = loaded);
|
||||
unawaited(registerCursors(_cursors));
|
||||
unawaited(CursorHandler.registerCursors(_cursors));
|
||||
}
|
||||
|
||||
Future<void> _reset() async {
|
||||
@@ -409,7 +409,7 @@ class _CursorCardState extends State<CursorCard> {
|
||||
}
|
||||
|
||||
Future<void> _observeDirection() async {
|
||||
_cancelDirectionObserver = await observeSystemDirection((_) {
|
||||
_cancelDirectionObserver = await CursorHandler.observeSystemDirection((_) {
|
||||
if (!_hovering || !mounted) return;
|
||||
// Re-apply the active cursor with the flipped orientation.
|
||||
_applyCursor();
|
||||
@@ -417,8 +417,8 @@ class _CursorCardState extends State<CursorCard> {
|
||||
}
|
||||
|
||||
void _applyCursor() {
|
||||
final rtl = effectiveDirection(context) == TextDirection.rtl;
|
||||
unawaited(setCursorAsset(widget.name, rtl: rtl));
|
||||
final rtl = CursorHandler.effectiveDirection(context) == TextDirection.rtl;
|
||||
unawaited(CursorHandler.setCursorAsset(widget.name, rtl: rtl));
|
||||
}
|
||||
|
||||
@override
|
||||
@@ -437,7 +437,7 @@ class _CursorCardState extends State<CursorCard> {
|
||||
},
|
||||
onExit: (_) {
|
||||
setState(() => _hovering = false);
|
||||
unawaited(resetCursor());
|
||||
unawaited(CursorHandler.resetCursor());
|
||||
},
|
||||
child: AnimatedContainer(
|
||||
duration: const Duration(milliseconds: 150),
|
||||
@@ -542,7 +542,7 @@ class _CursorPreview extends StatelessWidget {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final rtl = effectiveDirection(context) == TextDirection.rtl;
|
||||
final rtl = CursorHandler.effectiveDirection(context) == TextDirection.rtl;
|
||||
final width = cursor.image.width.toDouble();
|
||||
final height = cursor.image.height.toDouble();
|
||||
return Center(
|
||||
|
||||
+50
-136
@@ -9,46 +9,30 @@ import "cursor.dart";
|
||||
import "cursor_asset.dart";
|
||||
import "cursor_scaler.dart";
|
||||
|
||||
abstract class CursorHandler {
|
||||
/// The platform channel used to communicate cursor operations to the native
|
||||
/// side.
|
||||
const _channel = MethodChannel("custom_cursor");
|
||||
static const _channel = MethodChannel("custom_cursor");
|
||||
|
||||
/// Display-name suffix for the right-to-left (mirrored) variant of a cursor.
|
||||
const String _rtlSuffix = ">rtl";
|
||||
static const String _rtlSuffix = ">rtl";
|
||||
|
||||
/// Metadata for a registered animated cursor, used to drive the Dart-side
|
||||
/// playback timer. Keyed by the direction-agnostic asset name.
|
||||
class _AnimatedRegistration {
|
||||
_AnimatedRegistration({required this.frameDelays});
|
||||
|
||||
final List<Duration> frameDelays;
|
||||
int index = 0;
|
||||
}
|
||||
|
||||
final Map<String, _AnimatedRegistration> _animated = {};
|
||||
Timer? _animationTimer;
|
||||
ValueChanged<double>? _scaleObserver;
|
||||
ValueChanged<TextDirection>? _directionObserver;
|
||||
bool _handlerInstalled = false;
|
||||
bool _scaleListening = false;
|
||||
bool _directionListening = false;
|
||||
static final Map<String, _AnimatedRegistration> _animated = {};
|
||||
static Timer? _animationTimer;
|
||||
static ValueChanged<double>? _scaleObserver;
|
||||
static ValueChanged<TextDirection>? _directionObserver;
|
||||
static bool _handlerInstalled = false;
|
||||
static bool _scaleListening = false;
|
||||
static bool _directionListening = false;
|
||||
|
||||
/// The currently active asset name, so a live OS-direction change can re-apply
|
||||
/// the visible cursor.
|
||||
String? _activeAssetName;
|
||||
|
||||
/// The operating-system text/layout direction (macOS), updated live.
|
||||
final ValueNotifier<TextDirection> systemDirectionNotifier =
|
||||
ValueNotifier<TextDirection>(TextDirection.ltr);
|
||||
|
||||
/// Whether the platform should be queried for the OS direction. Used so tests
|
||||
/// and non-macOS platforms fall back gracefully.
|
||||
bool _queryOsDirection = true;
|
||||
static String? _activeAssetName;
|
||||
|
||||
/// Reads the current operating-system pointer-size multiplier (macOS
|
||||
/// accessibility setting), defaulting to `1.0` when unavailable or on
|
||||
/// unsupported platforms.
|
||||
Future<double> systemCursorScale() async {
|
||||
static Future<double> systemCursorScale() async {
|
||||
try {
|
||||
final value = await _channel.invokeMethod<double>("systemCursorScale");
|
||||
return value ?? 1.0;
|
||||
@@ -59,9 +43,13 @@ Future<double> systemCursorScale() async {
|
||||
}
|
||||
}
|
||||
|
||||
/// Whether the platform should be queried for the OS direction. Used so tests
|
||||
/// and non-macOS platforms fall back gracefully.
|
||||
static bool _queryOsDirection = true;
|
||||
|
||||
/// Reads the current operating-system layout direction (macOS), defaulting to
|
||||
/// left-to-right when unavailable or on unsupported platforms.
|
||||
Future<TextDirection> systemTextDirection() async {
|
||||
static Future<TextDirection> systemTextDirection() async {
|
||||
if (!_queryOsDirection) return systemDirectionNotifier.value;
|
||||
try {
|
||||
final value = await _channel.invokeMethod<String>("systemDirection");
|
||||
@@ -79,7 +67,7 @@ Future<TextDirection> systemTextDirection() async {
|
||||
/// Registers [onChanged] to be notified when the operating-system pointer-size
|
||||
/// multiplier changes while the app is running (macOS). Returns a function
|
||||
/// that stops the observation.
|
||||
Future<VoidCallback> observeSystemCursorScale(
|
||||
static Future<VoidCallback> observeSystemCursorScale(
|
||||
ValueChanged<double> onChanged,
|
||||
) async {
|
||||
_scaleObserver = onChanged;
|
||||
@@ -102,7 +90,7 @@ Future<VoidCallback> observeSystemCursorScale(
|
||||
/// Registers [onChanged] to be notified when the operating-system layout
|
||||
/// direction changes while the app is running (macOS). Returns a function
|
||||
/// that stops the observation.
|
||||
Future<VoidCallback> observeSystemDirection(
|
||||
static Future<VoidCallback> observeSystemDirection(
|
||||
ValueChanged<TextDirection> onChanged,
|
||||
) async {
|
||||
_directionObserver = onChanged;
|
||||
@@ -123,7 +111,7 @@ Future<VoidCallback> observeSystemDirection(
|
||||
};
|
||||
}
|
||||
|
||||
void _ensureHandler() {
|
||||
static void _ensureHandler() {
|
||||
if (_handlerInstalled) return;
|
||||
_handlerInstalled = true;
|
||||
_channel.setMethodCallHandler((call) async {
|
||||
@@ -138,20 +126,25 @@ void _ensureHandler() {
|
||||
systemDirectionNotifier.value = direction;
|
||||
_directionObserver?.call(direction);
|
||||
}
|
||||
return null;
|
||||
return;
|
||||
});
|
||||
}
|
||||
|
||||
/// The operating-system text/layout direction (macOS), updated live.
|
||||
static final ValueNotifier<TextDirection> systemDirectionNotifier =
|
||||
ValueNotifier<TextDirection>(TextDirection.ltr);
|
||||
|
||||
/// Resolves the effective layout direction for [context].
|
||||
///
|
||||
/// The enclosing widget [Directionality] wins when it is explicitly
|
||||
/// right-to-left. When the widget reports the default (left-to-right), the
|
||||
/// operating-system direction is used as a fallback.
|
||||
TextDirection effectiveDirection(
|
||||
static TextDirection effectiveDirection(
|
||||
BuildContext context, {
|
||||
TextDirection? osDirection,
|
||||
}) {
|
||||
final widgetDirection = Directionality.maybeOf(context) ?? TextDirection.ltr;
|
||||
final widgetDirection =
|
||||
Directionality.maybeOf(context) ?? TextDirection.ltr;
|
||||
if (widgetDirection == TextDirection.rtl) return TextDirection.rtl;
|
||||
final os = osDirection ?? systemDirectionNotifier.value;
|
||||
return os == TextDirection.rtl ? TextDirection.rtl : TextDirection.ltr;
|
||||
@@ -164,7 +157,7 @@ TextDirection effectiveDirection(
|
||||
/// under `name#<i>` (and its mirrored counterpart) and keep playback metadata
|
||||
/// for [setCursorAsset]. Best-effort: failures are ignored because the native
|
||||
/// side may not be ready yet.
|
||||
Future<void> registerCursors(Map<String, CursorAsset> cursors) async {
|
||||
static Future<void> registerCursors(Map<String, CursorAsset> cursors) async {
|
||||
await Future.wait(
|
||||
cursors.entries.map((entry) async {
|
||||
final asset = entry.value;
|
||||
@@ -187,12 +180,15 @@ Future<void> registerCursors(Map<String, CursorAsset> cursors) async {
|
||||
|
||||
/// Registers [cursor] both normally and mirrored (for RTL), disposing the
|
||||
/// source image after encoding.
|
||||
Future<void> _registerCursorPair(String name, Cursor cursor) async {
|
||||
static Future<void> _registerCursorPair(String name, Cursor cursor) async {
|
||||
await _registerCursor(name, cursor);
|
||||
await _registerCursor("$name$_rtlSuffix", await CursorScaler.mirror(cursor));
|
||||
await _registerCursor(
|
||||
"$name$_rtlSuffix",
|
||||
await CursorScaler.mirror(cursor),
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> _registerCursor(String name, Cursor cursor) async {
|
||||
static Future<void> _registerCursor(String name, Cursor cursor) async {
|
||||
final png = await _cursorPng(cursor.image);
|
||||
try {
|
||||
await _channel.invokeMethod("registerCursor", {
|
||||
@@ -208,7 +204,7 @@ Future<void> _registerCursor(String name, Cursor cursor) async {
|
||||
}
|
||||
}
|
||||
|
||||
Future<Uint8List> _cursorPng(ui.Image image) async {
|
||||
static Future<Uint8List> _cursorPng(ui.Image image) async {
|
||||
final data = await image.toByteData(format: ui.ImageByteFormat.png);
|
||||
return data!.buffer.asUint8List();
|
||||
}
|
||||
@@ -216,7 +212,7 @@ Future<Uint8List> _cursorPng(ui.Image image) async {
|
||||
/// Switches the platform cursor to the registered cursor [displayName] (which
|
||||
/// already encodes orientation via `>rtl`), or back to the system arrow when
|
||||
/// the name is unknown. Best-effort.
|
||||
Future<void> setCursor(String displayName) async {
|
||||
static Future<void> setCursor(String displayName) async {
|
||||
try {
|
||||
await _channel.invokeMethod("setCursor", {"name": displayName});
|
||||
} on PlatformException {
|
||||
@@ -229,7 +225,7 @@ Future<void> setCursor(String displayName) async {
|
||||
/// Switches the platform cursor to the registered asset [name], starting an
|
||||
/// animation loop for animated assets, honoring the [rtl] orientation. Falls
|
||||
/// back to the system arrow when the name is unknown. Best-effort.
|
||||
Future<void> setCursorAsset(String name, {bool rtl = false}) async {
|
||||
static Future<void> setCursorAsset(String name, {bool rtl = false}) async {
|
||||
_activeAssetName = name;
|
||||
final registration = _animated[name];
|
||||
if (registration == null) {
|
||||
@@ -242,7 +238,7 @@ Future<void> setCursorAsset(String name, {bool rtl = false}) async {
|
||||
}
|
||||
|
||||
/// Stops any running animation and restores the system arrow cursor.
|
||||
Future<void> resetCursor() async {
|
||||
static Future<void> resetCursor() async {
|
||||
_stopAnimation();
|
||||
_activeAssetName = null;
|
||||
await setCursor("arrow");
|
||||
@@ -250,13 +246,13 @@ Future<void> resetCursor() async {
|
||||
|
||||
/// Re-applies the cursor most recently shown via [setCursorAsset], using the
|
||||
/// [rtl] orientation. No-op when none is active.
|
||||
Future<void> reapplyCursor({required bool rtl}) async {
|
||||
static Future<void> reapplyCursor({required bool rtl}) async {
|
||||
final name = _activeAssetName;
|
||||
if (name == null) return;
|
||||
await setCursorAsset(name, rtl: rtl);
|
||||
}
|
||||
|
||||
void _startAnimation(String name, bool rtl) {
|
||||
static void _startAnimation(String name, bool rtl) {
|
||||
_animationTimer?.cancel();
|
||||
final registration = _animated[name];
|
||||
if (registration == null) return;
|
||||
@@ -268,7 +264,7 @@ void _startAnimation(String name, bool rtl) {
|
||||
_scheduleFrame(name, rtl);
|
||||
}
|
||||
|
||||
void _scheduleFrame(String name, bool rtl) {
|
||||
static void _scheduleFrame(String name, bool rtl) {
|
||||
final registration = _animated[name];
|
||||
if (registration == null) return;
|
||||
final delay = registration.frameDelays[registration.index];
|
||||
@@ -282,7 +278,7 @@ void _scheduleFrame(String name, bool rtl) {
|
||||
});
|
||||
}
|
||||
|
||||
void _stopAnimation() {
|
||||
static void _stopAnimation() {
|
||||
_animationTimer?.cancel();
|
||||
_animationTimer = null;
|
||||
_animated.forEach((_, registration) => registration.index = 0);
|
||||
@@ -291,98 +287,16 @@ void _stopAnimation() {
|
||||
/// Wraps a [Map] of static [Cursor]s (as produced by a skin loader) into
|
||||
/// [CursorAsset] values, so they can be passed to [registerCursors] or
|
||||
/// [CursorRegion].
|
||||
Map<String, CursorAsset> cursorAssets(Map<String, Cursor> cursors) {
|
||||
static Map<String, CursorAsset> cursorAssets(Map<String, Cursor> cursors) {
|
||||
return cursors.map((name, cursor) => MapEntry(name, StaticCursor(cursor)));
|
||||
}
|
||||
|
||||
/// Registers [cursors] once, then wraps [child] so hovering over it shows the
|
||||
/// default cursor (`normal` key) and leaving restores the system arrow. The
|
||||
/// shown cursor mirrors to the effective layout direction of [child]'s
|
||||
/// subtree, and re-mirrors live when the OS direction changes.
|
||||
class CursorRegion extends StatefulWidget {
|
||||
const CursorRegion({required this.cursors, required this.child, super.key});
|
||||
|
||||
final Map<String, CursorAsset> cursors;
|
||||
final Widget child;
|
||||
|
||||
@override
|
||||
State<CursorRegion> createState() => _CursorRegionState();
|
||||
}
|
||||
|
||||
class _CursorRegionState extends State<CursorRegion> {
|
||||
// Track the current cursor locally to prevent flooding the platform channel.
|
||||
String _activeCursorName = "arrow";
|
||||
bool _activeRtl = false;
|
||||
bool _isHovering = false;
|
||||
VoidCallback? _cancelDirectionObserver;
|
||||
/// Metadata for a registered animated cursor, used to drive the Dart-side
|
||||
/// playback timer. Keyed by the direction-agnostic asset name.
|
||||
class _AnimatedRegistration {
|
||||
_AnimatedRegistration({required this.frameDelays});
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
unawaited(registerCursors(widget.cursors));
|
||||
unawaited(_observeDirection());
|
||||
}
|
||||
|
||||
Future<void> _observeDirection() async {
|
||||
_cancelDirectionObserver = await observeSystemDirection((_) {
|
||||
if (!_isHovering || !mounted) return;
|
||||
// Re-apply the active cursor with the new orientation.
|
||||
_applyCursor(_activeCursorName);
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
void didUpdateWidget(CursorRegion oldWidget) {
|
||||
super.didUpdateWidget(oldWidget);
|
||||
if (!identical(oldWidget.cursors, widget.cursors)) {
|
||||
unawaited(registerCursors(widget.cursors));
|
||||
|
||||
// If the cursors change while the mouse is already inside the widget,
|
||||
// we need to re-evaluate what cursor should be showing.
|
||||
if (_isHovering) {
|
||||
_updateCursor();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_cancelDirectionObserver?.call();
|
||||
unawaited(resetCursor());
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
void _updateCursor() {
|
||||
final hasCustom = widget.cursors.containsKey("normal");
|
||||
_applyCursor(hasCustom ? "normal" : "arrow");
|
||||
}
|
||||
|
||||
void _applyCursor(String name) {
|
||||
final rtl = effectiveDirection(context) == TextDirection.rtl;
|
||||
if (_activeCursorName == name && _activeRtl == rtl) return;
|
||||
|
||||
_activeCursorName = name;
|
||||
_activeRtl = rtl;
|
||||
unawaited(setCursorAsset(name, rtl: rtl));
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return MouseRegion(
|
||||
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;
|
||||
_activeCursorName = "arrow";
|
||||
_activeRtl = false;
|
||||
unawaited(resetCursor());
|
||||
},
|
||||
child: widget.child,
|
||||
);
|
||||
}
|
||||
final List<Duration> frameDelays;
|
||||
int index = 0;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,96 @@
|
||||
import "dart:async";
|
||||
|
||||
import "package:custom_cursor/custom_cursor.dart";
|
||||
import "package:flutter/widgets.dart";
|
||||
|
||||
/// Registers [cursors] once, then wraps [child] so hovering over it shows the
|
||||
/// default cursor (`normal` key) and leaving restores the system arrow. The
|
||||
/// shown cursor mirrors to the effective layout direction of [child]'s
|
||||
/// subtree, and re-mirrors live when the OS direction changes.
|
||||
class CursorRegion extends StatefulWidget {
|
||||
const CursorRegion({required this.cursors, required this.child, super.key});
|
||||
|
||||
final Map<String, CursorAsset> cursors;
|
||||
final Widget child;
|
||||
|
||||
@override
|
||||
State<CursorRegion> createState() => _CursorRegionState();
|
||||
}
|
||||
|
||||
class _CursorRegionState extends State<CursorRegion> {
|
||||
// Track the current cursor locally to prevent flooding the platform channel.
|
||||
String _activeCursorName = "arrow";
|
||||
bool _activeRtl = false;
|
||||
bool _isHovering = false;
|
||||
VoidCallback? _cancelDirectionObserver;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
unawaited(CursorHandler.registerCursors(widget.cursors));
|
||||
unawaited(_observeDirection());
|
||||
}
|
||||
|
||||
Future<void> _observeDirection() async {
|
||||
_cancelDirectionObserver = await CursorHandler.observeSystemDirection((_) {
|
||||
if (!_isHovering || !mounted) return;
|
||||
// Re-apply the active cursor with the new orientation.
|
||||
_applyCursor(_activeCursorName);
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
void didUpdateWidget(CursorRegion oldWidget) {
|
||||
super.didUpdateWidget(oldWidget);
|
||||
if (!identical(oldWidget.cursors, widget.cursors)) {
|
||||
unawaited(CursorHandler.registerCursors(widget.cursors));
|
||||
|
||||
// If the cursors change while the mouse is already inside the widget,
|
||||
// we need to re-evaluate what cursor should be showing.
|
||||
if (_isHovering) {
|
||||
_updateCursor();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_cancelDirectionObserver?.call();
|
||||
unawaited(CursorHandler.resetCursor());
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
void _updateCursor() {
|
||||
final hasCustom = widget.cursors.containsKey("normal");
|
||||
_applyCursor(hasCustom ? "normal" : "arrow");
|
||||
}
|
||||
|
||||
void _applyCursor(String name) {
|
||||
final rtl = CursorHandler.effectiveDirection(context) == TextDirection.rtl;
|
||||
if (_activeCursorName == name && _activeRtl == rtl) return;
|
||||
|
||||
_activeCursorName = name;
|
||||
_activeRtl = rtl;
|
||||
unawaited(CursorHandler.setCursorAsset(name, rtl: rtl));
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return MouseRegion(
|
||||
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;
|
||||
_activeCursorName = "arrow";
|
||||
_activeRtl = false;
|
||||
unawaited(CursorHandler.resetCursor());
|
||||
},
|
||||
child: widget.child,
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user