custom_mouse_cursor 1.0.0 custom_mouse_cursor: ^1.0.0 copied to clipboard
Custom mouse cursors for Flutter which are devicePixelRatio aware.
import 'dart:async';
import 'dart:ui' as ui;
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:custom_mouse_cursor/custom_mouse_cursor.dart';
import 'package:google_fonts/google_fonts.dart';
import 'package:material_symbols_icons/sharp.dart';
import 'package:signature/signature.dart';
import 'package:chalkdart/chalk.dart';
class _Logger {
static void log(String message) {
debugPrint(message);
}
}
late CustomMouseCursor assetCursor;
late CustomMouseCursor assetCursorOnly25;
late CustomMouseCursor assetNative8x;
late CustomMouseCursor iconCursor;
late CustomMouseCursor msIconCursor;
late CustomMouseCursor assetCursorSingleSize;
late CustomMouseCursor catUiImageCursor;
Future<void> initializeCursors() async {
_Logger.log(chalk.brightRed("initializeCursors() Creating cursors from asset and from icon"));
CustomMouseCursor.useWebKitImageSet = true; // Optional flag to specify web platform to use `-webkit-image-set` command instead of `image-set` (defaults to true).
//CustomMouseCursor.useOnlyImageSetCSS = true; // Optional flag to specify web platform to use `image-set()` css commands only. (defaults to false).
//CustomMouseCursor.useOnlyURLDataURICursorCSS = true; // Optional flag to specify web platform use only `url()` css commands. (defaults to false).
// Example of image asset that has many device pixel ratio versions (1.5x,2.0x,2.5x,3.0x,3.5x,4.0x,8.0x).
// The exact size required for most DevicePixelRatio will be able to be loaded directly and used
// without scaling.
assetCursor = await CustomMouseCursor.asset(
"assets/cursors/startrek_mousepointer.png",
hotX: 18,
hotY: 0);
// Example of image asset that has only device pixel ratio versions (1.0x ands 2.5x).
// In this case if the devicePixelRatio was 2.0x the 2.5x asset would be loaded and
// scaled down to 2.0x size.
assetCursorOnly25 = await CustomMouseCursor.asset(
"assets/cursors/startrek_mousepointer25Only.png",
hotX: 18,
hotY: 0);
// Example of image asset only at 8x native DevicePixelRatio so will get scaled down
// to most/all encoutered DPR's.
assetNative8x = await CustomMouseCursor.exactAsset(
"assets/cursors/star-trek-mouse-pointer-cursor292x512.png",
hotX: 144,
hotY: 0,
nativeDevicePixelRatio: 8.0);
// Example of a custom cursor created from a icon, with drop shadow added.
List<Shadow> shadows = [
const BoxShadow(
color: Color.fromRGBO(0, 0, 0, 0.8),
offset: Offset(4, 3),
blurRadius: 3,
spreadRadius: 2,
),
];
iconCursor = await CustomMouseCursor.icon(Icons.redo,
size: 24,
hotX: 22,
hotY: 17,
color: Colors.pinkAccent,
shadows: shadows);
// example of custom cursor created from a icon that is filled, colored blue and
// added drop shadow.
msIconCursor = await CustomMouseCursor.icon(
MaterialSymbols.arrow_selector_tool,
size: 32,
hotX: 8,
hotY: 2,
fill: 1,
color: Colors.blueAccent,
shadows: shadows);
// another exactAsset example where the supplied image asset is a 2.0x image. This will
// be scaled down at 1.0x devicePixelRatios and scaled up for >2.0x device pixel ratios.
assetCursorSingleSize = await CustomMouseCursor.exactAsset(
"assets/cursors/example_game_cursor_64x64.png",
hotX: 2,
hotY: 2,
nativeDevicePixelRatio: 2.0);
// Now this is example of [image] use. This is intended for more of a 'power user' interface
// as it is lower level in that it accepts ui.Image objects. (Image from `import 'dart:ui'`).
var rawBytes = await rootBundle.load("assets/cursors/cat_cursor4xWithPinkShadow.png"); // 196x272
var rawUintList = rawBytes.buffer.asUint8List();
final catCursorUiImage4x = await decodeImageFromList(rawUintList);
rawBytes = await rootBundle.load("assets/cursors/cat_cursor2xWithBlueShadow.png"); // 98x136
rawUintList = rawBytes.buffer.asUint8List();
final ui.Image catCursorUiImage2x = await decodeImageFromList(rawUintList);
// We create the image cursor with the 4.0x image - this could be the only image needed and all
// devicePixelRatios will be drived from this image by scaling..
catUiImageCursor = await CustomMouseCursor.image(
catCursorUiImage4x,
hotX: 4,
hotY: 30,
thisImagesDevicePixelRatio: 4.0,
finalizeForCurrentDPR: false);
// but we can also add additional images at other DPR to supply specific images for those
// DPR without the need to scale.
// By looking at the color of the shadow you can tell which cursor image is being used.
// Experiment with commenting out the following and see that the shadow changes to pink.
const illustrateUseOfAddtionalImages = false;
if(illustrateUseOfAddtionalImages) {
await catUiImageCursor.addImage(
catCursorUiImage2x,
thisImagesDevicePixelRatio: 2.0);
}
await catUiImageCursor.finalizeImages();
}
void main() async {
WidgetsFlutterBinding.ensureInitialized();
/* Comment left here to illustrate options.
If the user wants to implement custom `onMetricsChanged()` handling there
is a mechanism using ` CustomMouseCursor.noOnMetricsChangedHook = true;`.
CustomMouseCursor.noOnMetricsChangedHook = true;
WidgetsBinding.instance.platformDispatcher.onMetricsChanged = () {
// you must call ensurePointersMatchDevicePixelRatio to handle devicePixelRatio changes
CustomMouseCursor.ensurePointersMatchDevicePixelRatio(null);
};
*/
await initializeCursors();
runApp(const MyApp());
}
class MyApp extends StatefulWidget {
const MyApp({Key? key}) : super(key: key);
@override
State<MyApp> createState() => _MyAppState();
}
class _MyAppState extends State<MyApp> with WidgetsBindingObserver {
late final SignatureController _signatureController;
late final Widget _signatureCanvas;
CustomMouseCursor? currentDrawCursor;
late Size _lastSize;
late double _lastDevicePixelRatio;
/*
Illustrate OPTIONAL (power user) use of manual onMetricsChanged handling.
Typically THERE IS NO NEED TO DO THIS.
THIS can only be used if `CustomMouseCursor.noOnMetricsChangedHook = true;` is defined (SEE ABOVE).
Additionally it will only get called if `WidgetsBinding.instance.platformDispatcher.onMetricsChanged`
is not hooked with a function above.
See for //OPTIONAL_didChangeMetrics_HOOKING// comments and remove those also.
@override
void didChangeMetrics() {
_Logger.log(chalk.brightYellow('WIDGETS didChangeMetrics() callback called!!!!'));
setState(() {
double prevDPR = _lastDevicePixelRatio;
_lastSize = WidgetsBinding.instance.window.physicalSize;
_lastDevicePixelRatio = WidgetsBinding.instance.window.devicePixelRatio;
_Logger.log(
chalk.yellow('setState() in didChangeMetrics() Window size is $_lastSize prevDPR=$prevDPR new DevicePixelRatio=$_lastDevicePixelRatio'));
CustomMouseCursor.ensurePointersMatchDevicePixelRatio(context);
});
}
*/
/// Change this to true illustrates the widget calling the CustomMouseCursor DPR handler function
/// during didChangeDependencies call.
static const illustrateManualHandlingOfDevicePixelRatioChangesForCustomCursors = false;
@override
void didChangeDependencies() {
super.didChangeDependencies();
_Logger.log(chalk.yellow('WIDGETS didChangeDependencies() callback called!!!!'));
// illustrate completely optional manual handling of DPR changes for CustomMouseCursor.
if(illustrateManualHandlingOfDevicePixelRatioChangesForCustomCursors) {
double prevDPR = _lastDevicePixelRatio;
_lastSize = WidgetsBinding.instance.window.physicalSize;
_lastDevicePixelRatio = WidgetsBinding.instance.window.devicePixelRatio;
_Logger.log(
chalk.yellow('in didChangeDependencies() Window size is $_lastSize prevDPR=$prevDPR new DevicePixelRatio=$_lastDevicePixelRatio'));
CustomMouseCursor.ensurePointersMatchDevicePixelRatio(context);
}
}
void selectCursorCallback(CustomMouseCursor cursor) {
_Logger.log('Changing to drawing area cursor to key=${cursor.key}');
setState(() {
currentDrawCursor = cursor;
_signatureController.clear();
});
}
@override
void initState() {
_Logger.log(chalk.brightRed('MyApp initState() called'));
super.initState();
_lastSize = WidgetsBinding.instance.window.physicalSize;
_lastDevicePixelRatio = WidgetsBinding.instance.window.devicePixelRatio;
_Logger.log(
chalk.red('Window size is $_lastSize _lastDevicePixelRatio=$_lastDevicePixelRatio'));
//OPTIONAL_didChangeMetrics_HOOKING//WidgetsBinding.instance.addObserver(this);
// call once in the initState() to
//CustomMouseCursor.ensurePointersMatchDevicePixelRatio(null);
// Initialise a controller. It will contains signature points, stroke width and pen color.
// It will allow you to interact with the widget
_signatureController = SignatureController(
penStrokeWidth: 1,
penColor: Colors.blueAccent,
exportBackgroundColor: Colors.blueGrey,
);
_signatureCanvas = Expanded(
child: Signature(
//width: 300,
height: 300,
controller: _signatureController,
backgroundColor: Colors.white,
));
}
@override
void dispose() {
super.dispose();
//OPTIONAL_didChangeMetrics_HOOKING//WidgetsBinding.instance.removeObserver(this);
_signatureController.dispose();
CustomMouseCursor.disposeAll();
}
@override
Widget build(BuildContext context) {
return MaterialApp(
home: Scaffold(
appBar: AppBar(
title: RichText(
text: TextSpan(
text: 'CustomMouseCursor Example and Interactive Test App',
style:
const TextStyle(fontSize: 20),
children: const <TextSpan>[
TextSpan(
text:
'\n (move window to monitor with different devicePixelRatio to support for test varying DPR)',
style:
TextStyle(fontWeight: FontWeight.bold, fontSize: 10)),
],
),
),
),
body: Center(
child: ListView(
children: [
CursorTesterSelectorRegion(
assetCursor,
message: 'Click to Select Asset Cursor',
note:
'(with 1.0x,1.5x,2.0x,2.5x,3.0x,3.5x,4.0x, and 8.0x assets present)',
note2: '(should appear as tall as this row)',
details:
'CustomMouseCursor.asset("assets/cursors/startrek_mousepointer.png", hotX:18, hotY:0)',
color: Colors.indigoAccent,
selectCursorCallback: selectCursorCallback,
),
CursorTesterSelectorRegion(
assetCursorOnly25,
message: 'Click to Select Asset (Only 1.0 & 2.5x) Cursor',
note:
'(only 1.0x and 2.5x assets present) (should be identical to above)',
details:
'CustomMouseCursor.asset("assets/cursors/startrek_mousepointer25Only.png", hotX:18, hotY:0)',
color: Colors.blue,
selectCursorCallback: selectCursorCallback,
),
CursorTesterSelectorRegion(
assetNative8x,
message: 'Click to Select 8x DPR ExactAsset Cursor',
note:
'(single 8.0x exactAsset specified) (should be identical to above)',
details:
'CustomMouseCursor.exactAsset("assets/cursors/star-trek-mouse-pointer-cursor292x512.png", hotX: 144, hotY: 0, nativeDevicePixelRatio: 8.0);',
color: const ui.Color.fromARGB(255, 26, 133, 172),
selectCursorCallback: selectCursorCallback,
),
CursorTesterSelectorRegion(
iconCursor,
message: 'Click to Select Icon Cursor',
details:
'CustomMouseCursor.icon( Icons.redo, size: 24, hotX:22, hotY:17, color:Colors.red, shadows:shadows)',
color: Colors.green,
selectCursorCallback: selectCursorCallback,
),
CursorTesterSelectorRegion(
msIconCursor,
message: 'Click to Select Material Symbols Icon Cursor',
note: '(using material_symbols_icons package)',
details:
'CustomMouseCursor.icon( MaterialSymbols.arrow_selector_tool, size: 32, hotX: 8, hotY: 2, fill: 1, color: Colors.blueAccent )',
color: Colors.yellow,
selectCursorCallback: selectCursorCallback,
),
CursorTesterSelectorRegion(
assetCursorSingleSize,
message: 'Click to Select ExactAsset Cursor',
note: '(DPR 2.0 asset/hotspot coords)',
details:
'CustomMouseCursor.exactAsset("assets/cursors/example_game_cursor_64x64.png", hotX: 2, hotY: 2, nativeDevicePixelRatio: 2.0)',
color: Colors.orange,
selectCursorCallback: selectCursorCallback,
),
CursorTesterSelectorRegion(
catUiImageCursor,
message: 'Click to Select Image Cursor',
note: '(created with a single ui.Image at DPR 4.0x)',
details:
'CustomMouseCursor.image( uiImage, hotX: 4, hotY: 30, thisImagesDevicePixelRatio: 4.0)',
color: Colors.redAccent,
selectCursorCallback: selectCursorCallback,
),
MouseRegion(
cursor: currentDrawCursor != null
? currentDrawCursor!
: SystemMouseCursors.precise,
child: Column(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Row( // todo: const but Not on stable channel
mainAxisAlignment: MainAxisAlignment.spaceAround,
children: [
Text(
"Click/drag to draw with mouse and to test cursor's hot spot (X,Y).",
style: TextStyle(
fontSize: 20, fontWeight: FontWeight.bold)),
]),
Row(children: [_signatureCanvas]),
]),
),
],
)),
),
);
}
}
typedef SelectCursorCallback = void Function(CustomMouseCursor);
class CursorTesterSelectorRegion extends StatelessWidget {
final Color color;
final SelectCursorCallback? selectCursorCallback;
final String message;
final String? note;
final String? note2;
final String? details;
final CustomMouseCursor cursor;
const CursorTesterSelectorRegion(this.cursor,
{super.key,
required this.message,
this.note,
this.note2,
this.details,
this.color = Colors.white,
this.selectCursorCallback});
@override
Widget build(BuildContext context) {
return MouseRegion(
cursor: cursor,
child: GestureDetector(
onTap: () {
selectCursorCallback?.call(cursor);
},
child: Container(
decoration: BoxDecoration(
color: color,
border: const Border(
bottom: BorderSide(color: Colors.black, width: 1),
),
),
child: Column(
mainAxisAlignment: MainAxisAlignment.spaceAround,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.baseline,
textBaseline: TextBaseline.alphabetic,
children: [
Text(message,
style: const TextStyle(
fontSize: 20,
fontWeight: FontWeight.bold,
)),
if (note != null) const SizedBox(width: 20),
if (note != null)
Text(note!,
style: const TextStyle(
fontSize: 16,
fontStyle: FontStyle.italic,
)),
if (note2 != null) const SizedBox(width: 10),
if (note2 != null)
Text(note2!,
style: const TextStyle(
fontSize: 16,
fontWeight: FontWeight.bold,
)),
]),
if (details != null)
Container(
decoration: BoxDecoration(
color: Colors.grey,
border: Border.all(color: Colors.white),
borderRadius: BorderRadius.circular(10.0)),
child: Padding(
padding: const EdgeInsets.all(7),
child: Text(details!,
style: //TextStyle(fontSize: 16, backgroundColor: Colors.grey),
GoogleFonts.jetBrainsMono(
fontSize: 12, backgroundColor: Colors.grey)),
),
),
const SizedBox(height: 8),
],
),
),
),
);
}
}