custom_mouse_cursor 1.0.0 copy "custom_mouse_cursor: ^1.0.0" to clipboard
custom_mouse_cursor: ^1.0.0 copied to clipboard

Custom mouse cursors for Flutter which are devicePixelRatio aware.

example/lib/main.dart

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),
            ],
          ),
        ),
      ),
    );
  }
}
16
likes
0
pub points
79%
popularity

Publisher

verified publisherhiveright.tech

Custom mouse cursors for Flutter which are devicePixelRatio aware.

Repository (GitHub)
View/report issues

License

unknown (license)

Dependencies

chalkdart, flutter

More

Packages that depend on custom_mouse_cursor