stylet 0.1.2 copy "stylet: ^0.1.2" to clipboard
stylet: ^0.1.2 copied to clipboard

Cross-platform Flutter stylus input with pressure, tilt, buttons, barrel rotation, hover, double-tap, and squeeze support.

Stylet package illustration

Stylet #

High-fidelity stylus input for Flutter on Android, iOS, Linux, macOS, Web, and Windows.

Stylet keeps Flutter's regular PointerEvent pipeline intact and enriches it with native values that Flutter does not expose everywhere: barrel rotation, tangential pressure, Apple Pencil double-tap and squeeze, native tilt axes, and device identifiers. Replaceable motion predictions and corrections for initially estimated Apple Pencil values are exposed separately from definitive input. On the Web, Pointer Events add raw/coalesced samples, tangential pressure, rotation, and browser-generated predictions; the experimental Ink API is an optional compositor trail. Stylet is designed for drawing applications such as Focale, but does not impose a canvas, brush engine, or state-management solution.

Installation #

Add Stylet to the consuming application's pubspec.yaml:

flutter pub add stylet

Then run flutter pub get.

Quick start #

Wrap an interactive canvas with StyletListener. The callback receives a normalized sample for every stylus pointer event, while the child continues to receive its original Flutter events.

import 'package:flutter/widgets.dart';
import 'package:stylet/stylet.dart';

StyletListener(
  behavior: HitTestBehavior.opaque,
  onEvent: (StylusMotionEvent event) {
    final double pressure = event.normalizedPressure ?? 1;
    final double tilt = event.tilt ?? 0;
    final double orientation = event.orientation ?? 0;
    final double barrelRotation = event.barrelRotation ?? 0;

    updateBrush(
      position: event.localPosition,
      pressure: pressure,
      tilt: tilt,
      orientation: orientation,
      barrelRotation: barrelRotation,
    );
  },
  onAction: (StylusActionEvent event) {
    final bool commitsPreferredAction = event.phase == StylusActionPhase.discrete || event.phase == StylusActionPhase.ended;
    if (commitsPreferredAction && event.preferredAction == StylusPreferredAction.switchEraser) {
      toggleEraser();
    }
    if (event.action == StylusAction.squeeze &&
        event.phase == StylusActionPhase.began) {
      showToolPalette(at: event.pose?.position);
    }
  },
  onPrediction: (StylusPredictionEvent event) {
    replacePredictedPath(event.pointerIdentifier, event.samples);
  },
  onCorrection: (StylusCorrectionEvent event) {
    replaceRecordedSample(event.sampleIdentifier, event.correctedSample);
  },
  child: const DocumentCanvas(),
)

StyletListener ignores mouse and touch input by default. Set includeNonStylus: true when one callback should normalize every pointer kind. Do not dispose Stylet.instance; create and dispose a dedicated Stylet controller only when an isolated lifecycle is needed.

Integrating an existing Listener #

If your editor already handles raw events in its document canvas, you can retain that listener and replace its local pressure/tilt extraction with one conversion:

final Stylet stylet = Stylet.instance;

void onPointerMove(PointerMoveEvent pointerEvent) {
  final StylusMotionEvent event = stylet.convertPointerEvent(
    event: pointerEvent,
  );
  brushController.extendStroke(
    position: event.localPosition,
    pressure: event.normalizedPressure ?? 1,
    tilt: event.tilt ?? 0,
    orientation: event.orientation ?? 0,
    barrelRotation: event.barrelRotation ?? 0,
  );
}

The controller maintains a short native-event cache and correlates samples by embedder identifier when available, then by timestamp, tool, phase, and position. event.source == StyletEventSource.combined indicates that a Flutter event received native extensions.

Stylus body interactions are independent of pointer movement:

final StreamSubscription<StylusActionEvent> subscription = Stylet.instance.actions.listen(handleStylusAction);

// Cancel this application-owned subscription with its lifecycle.
await subscription.cancel();

Predictions are temporary previews. Each event replaces the preceding preview for its pointer, including an empty sample list that clears it:

final StreamSubscription<StylusPredictionEvent> predictions = Stylet.instance.predictions.listen((event) {
  replacePredictedPath(
    pointerIdentifier: event.pointerIdentifier,
    samples: event.samples,
  );
});

final StreamSubscription<StylusCorrectionEvent> corrections = Stylet.instance.corrections.listen((event) {
  replaceRecordedSample(
    identifier: event.sampleIdentifier,
    sample: event.correctedSample,
  );
});

Cancel both application-owned subscriptions with their lifecycle. Prediction positions use Flutter-view coordinates like other native events; transform them through the receiving render object when drawing inside a nested widget.

Web applications can additionally opt into a browser-composited delegated ink trail. It is a visual latency optimization, not a replacement for authoritative StyletListener samples. See docs/web_support.md for feature detection, setup, and lifecycle details.

API model #

StylusMotionEvent uses consistent semantics across platforms:

  • positions and deltas are logical pixels relative to the Flutter view;
  • pressure and distance retain their device ranges, with normalized getters;
  • tilt is the angle in radians away from the surface normal;
  • orientation is the direction in radians of the projected stylus axis;
  • tiltX and tiltY are optional signed components in radians;
  • barrel rotation is clockwise radians around the stylus axis;
  • tangential pressure is normalized from -1 to 1;
  • wheelDelta is signed relative stylus-wheel movement in radians;
  • buttons use Flutter's public button bit field; use isSideButtonPressed(number: 1) instead of hard-coded masks;
  • sampleIdentifier correlates an estimated sample with a later correction;
  • estimated properties and properties still expecting updates are distinct, because a value can remain estimated after the platform stops refining it.

StylusCapabilities describes what the current backend can potentially provide. The features set on an individual motion sample is more precise and should be used when behavior depends on a value being present.

Native device and tablet-pad events are available independently of motion:

final StreamSubscription<StylusDeviceEvent> devices = Stylet.instance.deviceEvents.listen(handleDeviceChange);
final StreamSubscription<TabletPadEvent> controls = Stylet.instance.padEvents.listen(handleTabletControl);

final Map<String, StylusDevice> connected = Stylet.instance.connectedDevices;

Cancel application-owned subscriptions with their lifecycle. Linux reports pad buttons, rings, strips, and mapping modes through Wayland tablet-v2 or GTK; relative dials require tablet-v2 version 2.

Wacom controls on macOS and Wintab controls on Windows require the application to replace the driver's current per-application mappings. Enable that behavior only while your editing surface is ready to consume the events:

final bool controlsClaimed = await Stylet.instance.setTabletPadOverrideEnabled(enabled: true);

// Release explicitly when leaving the editing experience. Cancelling Stylet's
// native event stream or unloading the plugin also restores the driver.
await Stylet.instance.setTabletPadOverrideEnabled(enabled: false);

The call returns false when there is no compatible driver/control or when the platform uses passive pad delivery. macOS applications must also provide the Automation usage description and entitlement described in docs/native_backends.md.

Platform support #

Feature Android iOS/iPadOS Linux macOS Web Windows
Pressure, tilt, orientation Yes Yes Yes Yes Pointer Events Yes
Hover pose Yes iPadOS 16.1+ Yes Yes Pointer Events Yes
Side buttons Yes No; double-tap/squeeze only Yes Yes First barrel button Yes
Eraser tool Yes Virtual via switchEraser Yes Yes Pointer Events Yes
Barrel rotation Driver AXIS_RZ iOS 17.5+ GTK or tablet-v2 AppKit rotation PointerEvent.twist Windows Ink
Tangential pressure GTK or tablet-v2 AppKit barrel pressure Pointer Events Wintab driver, when present
Stylus wheel Wayland tablet-v2
High-rate sample delivery Motion history Coalesced touches Protocol frames Coalescing disabled Coalesced/raw updates Windows Ink history batches
Predicted trajectories AndroidX Input UIKit Browser, when available Experimental opt-in
Delegated compositor trail Experimental Ink API
Estimated-value corrections UIKit
Device identity / metadata InputManager GTK or tablet-v2 AppKit PE4 session ID, if available Windows Ink
Pad buttons, rings, strips GTK or tablet-v2 + libwacom metadata Wacom DRI, explicit opt-in Wintab, explicit opt-in
Relative pad dials Tablet-v2 version 2
Double-tap Apple Pencil + preference Sidecar / AppKit
Squeeze iOS 17.5+, Apple Pencil Pro

Hardware and tablet drivers determine whether an advertised axis produces meaningful values. Pen-motion backends observe input passively and return the native event unchanged, so Flutter's gesture arena remains authoritative. The only active takeover is the explicitly requested Wacom/Wintab pad override described above.

The iOS deployment target is 15.0 and the macOS deployment target is 12.0. Other targets follow the minimum versions supported by the current Flutter toolchain.

Native event contract #

Backend maintainers can find channel names, required packet fields, units, and lifecycle rules in docs/platform_contract.md. Native dependencies and fallback behavior are detailed in docs/native_backends.md. Web Pointer Events and delegated ink behavior are detailed in docs/web_support.md. The runnable application in example/ visualizes capabilities, pressure, tilt, barrel angle, stylus-wheel movement, side buttons, body actions, predictions, corrections, native devices, and tablet-pad controls.


Built for Focale, an advanced local image editor. Discover what these packages make possible in a real creative workflow.

0
likes
160
points
109
downloads
screenshot

Documentation

API reference

Publisher

verified publisherfocale-editor.app

Weekly Downloads

Cross-platform Flutter stylus input with pressure, tilt, buttons, barrel rotation, hover, double-tap, and squeeze support.

Repository (GitHub)
View/report issues
Contributing

Topics

#stylus #pen #input #drawing #writing

License

MIT (license)

Dependencies

flutter, flutter_web_plugins, plugin_platform_interface, web

More

Packages that depend on stylet

Packages that implement stylet