Skip to content

raldhafiri/native_mouse_cursor

Folders and files

NameName
Last commit message
Last commit date

Latest commit

Β 

History

20 Commits
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 

Repository files navigation

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.
  • ↔️ Infinite drag β€” InfiniteDragRegion gives an unbounded value scrub on every platform (desktop edge-warp, web Pointer Lock incl. Firefox), with a cursor that wraps the viewport on web.
  • πŸ“¦ 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.2.0
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 never runs out of room. Wrap your handle in InfiniteDragRegion and you're done β€” it handles every platform and browser for you, and hands you the effective delta to apply each frame:

double value = 0;

InfiniteDragRegion(
  // Optional: a baked cursor. While locked on web it's painted WRAPPING the
  // viewport so it never disappears.
  cursor: NativeMouseCursor.get('scrub', fallback: SystemMouseCursors.resizeLeftRight),
  onScrub: (delta) => setState(() => value += delta.dx * scrubRate),
  onActiveChanged: (active) => setState(() => _dragging = active), // optional
  child: Text('$value'),
);

Platform mechanism

InfiniteDragRegion uses one of two models, chosen per platform via canWarpPointer(). On macOS, Windows and Linux β€” X11, and Wayland on modern compositors β€” it warps: the OS teleports the visible cursor back from the edge while your drag events drive the value. On the web and on older Wayland compositors it locks: the pointer is hidden/frozen and an unbounded relative-motion stream drives the value, with a painted cursor wrapping the viewport (on Firefox the lock engages on a click). Mobile has neither, so the drag simply stops at the edge. A single Linux build serves both X11 and Wayland.

Lower-level control

Prefer to own the gesture? InfiniteDragController is still public β€” feed it the pointer + viewport and it returns the effective dx (warp-jump frame 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,
);

πŸ’‘ That snippet is the desktop warp path. The lock-based paths (web, Wayland) and Firefox's click-to-engage are exactly what InfiniteDragRegion wires up for you.

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 and the web. For the exact per-platform APIs/protocols, the Firefox click-to-engage model and the Linux 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.

About

Use any image as a real native OS mouse cursor (macOS/Windows/Linux/Web).

Topics

Resources

License

Stars

1 star

Watchers

0 watching

Forks

Packages

 
 
 

Contributors