native_mouse_cursor 1.1.0 copy "native_mouse_cursor: ^1.1.0" to clipboard
native_mouse_cursor: ^1.1.0 copied to clipboard

Turn any image, SVG, or painted glyph into a real OS mouse cursor on Flutter desktop, web & Android.

[native_mouse_cursor logo]

native_mouse_cursor #

Turn any image, SVG, or painted glyph into a real OS mouse cursor โ€” on Flutter desktop, web & Android.

pub package live demo license platforms

๐Ÿš€ Try the live web demo โ†’ (web preview โ€” full native cursors shine on macOS / Windows / Linux / Android)

[native_mouse_cursor demo โ€” a custom arrow cursor rotates to aim, mirrors across quadrants, and casts a baked drop shadow]

Unlike a cursor "painted" inside Flutter (a widget that chases the pointer), a NativeMouseCursor is handed to the operating system, so the OS compositor draws it for you. ๐Ÿช„

โœจ Why use it #

  • โšก Zero lag โ€” tracks the hardware pointer exactly, with no one-frame trail.
  • ๐Ÿซง No jitter โ€” a shadow or glow baked into the bitmap never shimmers, even while the cursor rotates.
  • ๐Ÿ”Œ Drop-in โ€” it's a real MouseCursor, so it works anywhere a SystemMouseCursors value does (MouseRegion, InkWell, scrollbars, โ€ฆ).
  • ๐Ÿ” Rotation & mirroring โ€” spin a glyph by angle or flip it on demand; each variant is baked and cached automatically.
  • ๐ŸŒ‘ Baked drop shadows โ€” CSS-style shadows rendered into the bitmap, so they stay rock-steady at every angle.
  • ๐Ÿ–ฅ๏ธ HiDPI-crisp โ€” bakes at your device pixel ratio and re-bakes on change.
  • ๐Ÿ–Œ๏ธ Optional painted overlay โ€” on web/desktop, opt into an in-app overlay that hides the system cursor and paints a perfectly seamless per-region one.
  • ๐Ÿ“ฆ SPM-first on macOS โ€” no CocoaPods required.

๐Ÿงฉ Platform support #

Platform Backend Status
[] macOS NSCursor (Swift, SPM) โœ… Supported
[] Windows HCURSOR (Win32) โœ… Supported
[] Linux GdkCursor (GTK) โœ… Supported
[] Android PointerIcon (API 24+) โœ… Supported ยฒ
[] Web CSS url(...) cursor โœ… Supported ยน
[] iOS / iPadOS system pointer โŒ Not possible ยณ

ยน Each cursor is applied as a CSS cursor: url(...) value, sized in logical px and capped at 128 px (browsers draw a cursor image at its intrinsic pixels and Chrome ignores larger ones). For HiDPI crispness it also emits a device-resolution image via image-set(โ€ฆ 2x), with the plain url() as a fallback. For a perfectly seamless per-region cursor, wrap your app in NativeMouseCursorOverlay(force: true) to paint the glyph and hide the CSS cursor instead.

ยฒ Native PointerIcon for tablets/Chromebooks with a connected mouse, trackpad or stylus on API 24+. On older devices the system pointer is used. For a rotating cursor, prefer the painted overlay โ€” rapid PointerIcon swaps flicker on Android.

ยณ iPadOS draws and manages the pointer itself โ€” there's no API to install an arbitrary bitmap cursor, nor to hide the system pointer, so the system pointer is used. (A painted overlay would just show through it as a double cursor.) iPhone is touch-only โ€” no pointer to replace.

๐Ÿ“ฆ Install #

dependencies:
  native_mouse_cursor: ^1.0.1
flutter pub add native_mouse_cursor

๐Ÿš€ Quick start #

The whole API is: register a source under an id, then get it. ๐ŸŽฏ

Everything hard โ€” loading the glyph, rotation, the baked drop shadow, automatic bitmap sizing, the angle-keyed cache, background warming and DPR re-baking โ€” lives in the package.

Mix NativeMouseCursorMixin into your State and the rest is automatic: it points the cache at the context's devicePixelRatio (re-baking on a DPR change) and rebuilds when a cursor finishes baking โ€” so you can call svg / get straight from build():

import 'package:native_mouse_cursor/native_mouse_cursor.dart';

class _MyState extends State<MyWidget> with NativeMouseCursorMixin {
  @override
  void initState() {
    super.initState();
    // ๐Ÿ“ Register here, NOT in build() โ€” svg() kicks off an async load + bake,
    // so it's a one-time side effect. For an SVG asset that's the whole call;
    // size, shadow and the hotspot all default.
    NativeMouseCursor.svg('rotate', 'assets/icons/rotate.svg');
    //   size:   defaults to the SVG's own (viewBox) size
    //   shadow: defaults to x:0 y:1 blur:1.5 black 50% (ฯƒ=blur/2); null = none
  }

  @override
  Widget build(BuildContext context) {
    // ๐Ÿ” build() only fetches โ€” the bitmap is baked + cached per angle on
    // demand, and the mixin rebuilds when a fresh one lands.
    return MouseRegion(
      // get() never returns null: until the bitmap is baked it returns
      // SystemMouseCursors.basic, so no `??` is needed.
      cursor: NativeMouseCursor.get('rotate', angle: handleAngleRadians),
      child: handle,
    );
  }
}

๐Ÿ’ก NativeMouseCursor.has(id) lets you guard a one-off lazy registration if you can't register up front. Prefer not to use the mixin? Call NativeMouseCursor.configure(devicePixelRatio:, onReady:) yourself once (and again whenever the DPR changes) instead.

๐ŸŽจ Cursor sources #

Pick the register call that matches your glyph โ€” all take the same id, size, shadow and hotspot options:

Call Glyph source
๐Ÿ–ผ๏ธ NativeMouseCursor.svg an SVG asset path (re-rasterised from vector)
๐ŸŒ… NativeMouseCursor.image a decoded ui.Image
โœ๏ธ NativeMouseCursor.draw a CursorPainter you paint into a box yourself
๐Ÿ› ๏ธ NativeMouseCursor.builder produce the bitmap yourself per angle + DPR
NativeMouseCursor.image('pointer', myUiImage, size: const Size(24, 24));

๐Ÿ” Rotation #

There's no rotation flag โ€” just the angle you pass to get. A fixed cursor is simply one you always fetch at the default angle (0), so a single bitmap is baked and reused:

NativeMouseCursor.svg('resize-h', 'assets/resize-h.svg');   // โ†”
// ...
cursor: NativeMouseCursor.get('resize-h'),

For a glyph that turns with a handle, vary the angle โ€” each rotation bucket is baked and cached the first time it's requested (the at-rest angle is warmed in the background; the nearest already-baked angle is shown meanwhile). The bitmap box is always sized for the glyph's diagonal, so it never clips as it turns. ๐ŸŒ€

โ†”๏ธ Mirroring #

flipX / flipY are resolved at get time, so one registered glyph yields a mirrored pair on demand โ€” no second asset:

NativeMouseCursor.svg('hand', 'assets/hand-right.svg');
// the same glyph, flipped โ€” a left hand from the right-hand asset:
cursor: NativeMouseCursor.get('hand', flipX: pointingLeft),

Every (angle, flip) combination is baked and cached the first time it's asked for; the unflipped variant is warmed in the background.

๐ŸŽฏ Hotspot #

By default the click point is the glyph's centre. To anchor it elsewhere (e.g. a tip-anchored pointer), pass hotspot in the glyph's own coords (its size / SVG viewBox, origin top-left) โ€” the package centres the glyph in the auto-sized bitmap and maps the hotspot in for you, so you never deal with box coordinates:

// A 32ร—32 arrow whose tip is at (9, 3):
NativeMouseCursor.svg('pointer', 'assets/icons/pointer.svg',
    hotspot: const Offset(9, 3));

๐Ÿ–ฅ๏ธ High-DPI & disposing #

Cursors bake at the DPR passed to configure and re-bake automatically when you call configure again with a new one, so they stay crisp on Retina/HiDPI. Release them when you're done:

NativeMouseCursor.dispose('rotate');  // ๐Ÿงน one cursor
NativeMouseCursor.disposeAll();       // ๐Ÿงผ everything

โ†”๏ธ Infinite drag (relative / warp) #

Infinite drag: drag a number (or any handle) and the value keeps changing forever because the pointer wraps at the window edge instead of running into it. InfiniteDragController owns all the per-OS work โ€” you keep your own gesture and just feed it the pointer + viewport, and it returns the effective dx to apply (warp-jump frame already skipped, edge already wrapped):

final _drag = InfiniteDragController();

GestureDetector(
  onHorizontalDragStart: (d) => _drag.start(d.globalPosition),
  onHorizontalDragUpdate: (d) async {
    final dx = await _drag.update(
      globalPosition: d.globalPosition,
      delta: d.delta,
      viewportSize: MediaQuery.sizeOf(context),
    );
    setState(() => value += dx * scrubRate);
  },
  onHorizontalDragEnd: (_) => _drag.end(),
  onHorizontalDragCancel: () => _drag.cancel(),
  child: handle,
);

๐Ÿ’ก The snippet above is the desktop warp path. To also support web and Wayland (where the OS stops sending motion to Flutter while the pointer is locked), pass an onLockedDelta callback to start() and apply the same scrub there โ€” see doc/infinite_drag.md.

The low-level primitive is also public: NativeMouseCursor.warpPointer(x, y) teleports the OS pointer to logical window coords, and NativeMouseCursor.canWarpPointer() reports whether the host supports it.

Works on macOS, Windows, Linux (X11 and Wayland) and the web โ€” each via the right native mechanism (cursor warp on most desktops; pointer-lock on Wayland and the web). The controller hides all of that, so your code stays the same everywhere. Mobile falls back to an ordinary clamped drag. For the per-platform mechanisms, the Wayland lock approach and the build requirements, see doc/infinite_drag.md.

๐Ÿ–Œ๏ธ Painted overlay (web / desktop) #

Want the cursor painted inside Flutter instead of as a real OS cursor? Wrap your app in NativeMouseCursorOverlay(force: true): it hides the system cursor and paints the same baked bitmap at the live pointer position.

MaterialApp(
  builder: (context, child) =>
      NativeMouseCursorOverlay(force: kIsWeb, child: child!),
  home: const MyHomePage(),
);

This is useful where the system cursor can actually be hidden:

  • [] Web โ€” a perfectly seamless per-region cursor (the engine's CSS handling is best-effort across regions); the CSS cursor is hidden.
  • [] Android โ€” recommended for a rotating cursor: the native PointerIcon flickers when swapped rapidly (an OS quirk), so the painted overlay (system pointer hidden) gives smooth rotation.
  • [] [] [] macOS / Windows / Linux โ€” preview the painted cursor (the native cursor is already pixel-perfect, so you rarely need this).

Off by default; the widget is a transparent pass-through unless force is set.

โš ๏ธ The overlay is a Flutter widget chasing the pointer, so it has a one-frame lag a real OS cursor doesn't. It only works where the system cursor can be hidden โ€” not on iOS/iPadOS (the system pointer can't be hidden, so a painted one would just double it).

๐Ÿงช Example #

The example/ app is an interactive showcase โ€” rotation (an arrow that aims at a dot), mirroring (flipX/flipY), the hotspot (a red dot marking the true pointer position), the baked shadow, and all four cursor sources โ€” plus a switch to toggle the painted overlay.

cd example && flutter run -d macos   # or -d chrome / windows / linux

โš™๏ธ How it works #

NativeMouseCursor extends Flutter's MouseCursor. When the framework activates the cursor for a pointer, the plugin asks the host to make the matching OS cursor current (NSCursor.set() / SetCursor / gdk_window_set_cursor). Because activation flows through Flutter's own cursor machinery, the OS cursor isn't fought over by the engine's system-cursor handling. ๐Ÿค

With NativeMouseCursorOverlay(force: true), activation is intercepted instead: it keeps the baked bitmaps, hides the system cursor, and paints the active cursor at the live pointer position.

๐Ÿ‘ค Author #

Rami Al-Dhafiri.

๐Ÿ“„ License #

MIT ยฉ Rami Al-Dhafiri.

1
likes
0
points
307
downloads

Publisher

unverified uploader

Weekly Downloads

Turn any image, SVG, or painted glyph into a real OS mouse cursor on Flutter desktop, web & Android.

Repository (GitHub)
View/report issues

Topics

#cursor #mouse #pointer #ui #desktop

License

unknown (license)

Dependencies

flutter, flutter_svg, flutter_web_plugins, plugin_platform_interface, web

More

Packages that depend on native_mouse_cursor

Packages that implement native_mouse_cursor