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.

example/lib/main.dart

import 'dart:async';
import 'dart:math' as math;

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

/// Starts the interactive Stylet example.
void main() => runApp(const StyletExampleApp());

/// Demonstrates every portable and native stylus value exposed by Stylet.
class StyletExampleApp extends StatelessWidget {
  /// Creates the example application.
  const StyletExampleApp({super.key});

  @override
  Widget build(BuildContext context) => MaterialApp(
    debugShowCheckedModeBanner: false,
    title: 'Stylet example',
    theme: ThemeData(
      colorScheme: ColorScheme.fromSeed(
        seedColor: const Color(0xff6750a4),
        brightness: Brightness.dark,
      ),
      useMaterial3: true,
    ),
    home: const _StylusLaboratory(),
  );
}

/// Interactive canvas that visualizes live stylus samples.
class _StylusLaboratory extends StatefulWidget {
  /// Creates the stylus laboratory page.
  const _StylusLaboratory();

  @override
  State<_StylusLaboratory> createState() => _StylusLaboratoryState();
}

/// Holds the latest sample, action, capabilities, and short pointer trail.
class _StylusLaboratoryState extends State<_StylusLaboratory> {
  /// Maximum number of points retained by the trail painter.
  static const int _maximumTrailLength = 160;

  /// Shared plugin controller used by the demo.
  final Stylet _stylet = Stylet.instance;

  /// Recent local pointer positions rendered on the canvas.
  final List<Offset> _trail = [];

  /// Latest normalized pointer sample.
  StylusMotionEvent? _motion;

  /// Latest double-tap or squeeze interaction.
  StylusActionEvent? _action;

  /// Platform features resolved asynchronously after startup.
  StylusCapabilities? _capabilities;

  /// Subscription that refreshes native device telemetry.
  StreamSubscription<StylusDeviceEvent>? _deviceSubscription;

  /// Subscription that refreshes graphics-tablet pad telemetry.
  StreamSubscription<TabletPadEvent>? _padSubscription;

  /// Latest native device connection or metadata change.
  StylusDeviceEvent? _deviceEvent;

  /// Latest graphics-tablet pad control change.
  TabletPadEvent? _padEvent;

  /// Latest replaceable predicted trajectory.
  StylusPredictionEvent? _prediction;

  /// Latest correction for an initially estimated sample.
  StylusCorrectionEvent? _correction;

  @override
  void initState() {
    super.initState();
    _deviceSubscription = _stylet.deviceEvents.listen(_handleDeviceEvent);
    _padSubscription = _stylet.padEvents.listen(_handlePadEvent);
    _loadCapabilities();
  }

  @override
  void dispose() {
    unawaited(_deviceSubscription?.cancel());
    unawaited(_padSubscription?.cancel());
    super.dispose();
  }

  @override
  Widget build(BuildContext context) => Scaffold(
    appBar: AppBar(title: const Text('Stylet input laboratory')),
    body: SafeArea(
      child: Padding(
        padding: const EdgeInsets.all(16),
        child: Column(
          crossAxisAlignment: CrossAxisAlignment.stretch,
          children: [
            Text(
              'Draw, hover, rotate the barrel, or use a stylus body gesture.',
              style: Theme.of(context).textTheme.bodyLarge,
            ),
            const SizedBox(height: 12),
            _CapabilityStrip(capabilities: _capabilities),
            const SizedBox(height: 12),
            Expanded(
              child: StyletListener(
                stylet: _stylet,
                behavior: HitTestBehavior.opaque,
                onEvent: _handleMotion,
                onAction: _handleAction,
                onPrediction: _handlePrediction,
                onCorrection: _handleCorrection,
                child: DecoratedBox(
                  decoration: BoxDecoration(
                    color: Theme.of(context).colorScheme.surfaceContainer,
                    borderRadius: BorderRadius.circular(20),
                    border: Border.all(
                      color: Theme.of(context).colorScheme.outlineVariant,
                    ),
                  ),
                  child: ClipRRect(
                    borderRadius: BorderRadius.circular(20),
                    child: CustomPaint(
                      painter: _StylusTrailPainter(
                        points: List.unmodifiable(_trail),
                        motion: _motion,
                      ),
                      child: const SizedBox.expand(),
                    ),
                  ),
                ),
              ),
            ),
            const SizedBox(height: 12),
            _TelemetryPanel(
              motion: _motion,
              action: _action,
              deviceEvent: _deviceEvent,
              padEvent: _padEvent,
              prediction: _prediction,
              correction: _correction,
              deviceCount: _stylet.connectedDevices.length,
            ),
          ],
        ),
      ),
    ),
  );

  /// Resolves backend capabilities while tolerating a disappearing page.
  Future<void> _loadCapabilities() async {
    final StylusCapabilities capabilities = await _stylet.capabilities;
    if (mounted) {
      setState(() => _capabilities = capabilities);
    }
  }

  /// Stores one motion sample and maintains a bounded visual trail.
  void _handleMotion(StylusMotionEvent event) {
    setState(() {
      _motion = event;
      if (event.phase == StylusPhase.down) {
        _trail.clear();
      }
      if (event.phase == StylusPhase.down || event.phase == StylusPhase.move) {
        _trail.add(event.localPosition);
        if (_trail.length > _maximumTrailLength) {
          _trail.removeRange(0, _trail.length - _maximumTrailLength);
        }
      }
    });
  }

  /// Stores the latest stylus body interaction for inspection.
  void _handleAction(StylusActionEvent event) =>
      setState(() => _action = event);

  /// Stores the latest native device change for inspection.
  void _handleDeviceEvent(StylusDeviceEvent event) =>
      setState(() => _deviceEvent = event);

  /// Stores the latest graphics-tablet control event for inspection.
  void _handlePadEvent(TabletPadEvent event) =>
      setState(() => _padEvent = event);

  /// Stores the latest replacement prediction for inspection.
  void _handlePrediction(StylusPredictionEvent event) =>
      setState(() => _prediction = event);

  /// Stores the latest estimated-property correction for inspection.
  void _handleCorrection(StylusCorrectionEvent event) =>
      setState(() => _correction = event);
}

/// Displays the advanced features offered by the current backend.
class _CapabilityStrip extends StatelessWidget {
  /// Capability description, or `null` while it is loading.
  final StylusCapabilities? capabilities;

  /// Creates a horizontally wrapping capability list.
  const _CapabilityStrip({required this.capabilities});

  @override
  Widget build(BuildContext context) {
    final StylusCapabilities? value = capabilities;
    if (value == null) {
      return const LinearProgressIndicator();
    }
    return Wrap(
      spacing: 6,
      runSpacing: 6,
      children: [
        for (final StylusFeature feature in value.features)
          Chip(label: Text(feature.name)),
      ],
    );
  }
}

/// Displays numerical data from the most recent stylus events.
class _TelemetryPanel extends StatelessWidget {
  /// Latest normalized motion sample.
  final StylusMotionEvent? motion;

  /// Latest stylus body interaction.
  final StylusActionEvent? action;

  /// Latest native device connection or metadata change.
  final StylusDeviceEvent? deviceEvent;

  /// Latest graphics-tablet pad control change.
  final TabletPadEvent? padEvent;

  /// Latest replaceable trajectory prediction.
  final StylusPredictionEvent? prediction;

  /// Latest definitive update for an estimated sample.
  final StylusCorrectionEvent? correction;

  /// Number of native devices currently tracked by the controller.
  final int deviceCount;

  /// Creates a telemetry panel from optional live data.
  const _TelemetryPanel({
    required this.motion,
    required this.action,
    required this.deviceEvent,
    required this.padEvent,
    required this.prediction,
    required this.correction,
    required this.deviceCount,
  });

  @override
  Widget build(BuildContext context) {
    final StylusMotionEvent? sample = motion;
    final String? preferredAction = action?.preferredAction?.name;
    final String actionLabel = action == null
        ? '—'
        : '${action!.action.name} · ${action!.phase.name}'
              '${preferredAction == null ? '' : ' · $preferredAction'}';
    return Card(
      margin: EdgeInsets.zero,
      child: Padding(
        padding: const EdgeInsets.all(14),
        child: Wrap(
          spacing: 24,
          runSpacing: 10,
          children: [
            _Metric(label: 'Phase', value: sample?.phase.name ?? '—'),
            _Metric(label: 'Tool', value: sample?.tool.name ?? '—'),
            _Metric(
              label: 'Pressure',
              value: _formatNumber(sample?.normalizedPressure),
            ),
            _Metric(label: 'Tilt', value: _formatDegrees(sample?.tilt)),
            _Metric(
              label: 'Azimuth',
              value: _formatDegrees(sample?.orientation),
            ),
            _Metric(
              label: 'Barrel',
              value: _formatDegrees(sample?.barrelRotation),
            ),
            _Metric(label: 'Wheel', value: _formatDegrees(sample?.wheelDelta)),
            _Metric(
              label: 'Button 1',
              value: sample?.isSideButtonPressed(number: 1) ?? false
                  ? 'down'
                  : 'up',
            ),
            _Metric(label: 'Action', value: actionLabel),
            _Metric(label: 'Devices', value: deviceCount.toString()),
            _Metric(
              label: 'Device event',
              value: _formatDeviceEvent(deviceEvent),
            ),
            _Metric(label: 'Pad event', value: _formatPadEvent(padEvent)),
            _Metric(
              label: 'Prediction',
              value: '${prediction?.samples.length ?? 0} samples',
            ),
            _Metric(
              label: 'Correction',
              value: correction == null
                  ? '—'
                  : correction!.correctedProperties
                        .map((property) => property.name)
                        .join(', '),
            ),
          ],
        ),
      ),
    );
  }
}

/// One compact label-value pair in the telemetry panel.
class _Metric extends StatelessWidget {
  /// Short metric label.
  final String label;

  /// Human-readable current value.
  final String value;

  /// Creates a telemetry metric.
  const _Metric({required this.label, required this.value});

  @override
  Widget build(BuildContext context) => SizedBox(
    width: 132,
    child: Column(
      crossAxisAlignment: CrossAxisAlignment.start,
      children: [
        Text(
          label,
          style: Theme.of(context).textTheme.labelMedium
              ?.copyWith(color: Theme.of(context).colorScheme.onSurfaceVariant),
        ),
        Text(
          value,
          maxLines: 1,
          overflow: TextOverflow.ellipsis,
          style: Theme.of(context).textTheme.bodyLarge,
        ),
      ],
    ),
  );
}

/// Paints a pressure-sensitive trail and a rotated stylus nib preview.
class _StylusTrailPainter extends CustomPainter {
  /// Recent local pointer positions.
  final List<Offset> points;

  /// Latest sample controlling width and nib angle.
  final StylusMotionEvent? motion;

  /// Creates a painter from immutable input snapshots.
  const _StylusTrailPainter({required this.points, required this.motion});

  @override
  void paint(Canvas canvas, Size size) {
    final StylusMotionEvent? sample = motion;
    final double pressure = sample?.normalizedPressure ?? 0.5;
    final Paint trailPaint = Paint()
      ..color = const Color(0xffd0bcff)
      ..strokeCap = StrokeCap.round
      ..strokeJoin = StrokeJoin.round
      ..strokeWidth = 2 + pressure * 14
      ..style = PaintingStyle.stroke;
    if (points.length > 1) {
      final Path path = Path()..moveTo(points.first.dx, points.first.dy);
      for (final Offset point in points.skip(1)) {
        path.lineTo(point.dx, point.dy);
      }
      canvas.drawPath(path, trailPaint);
    }
    if (sample == null) {
      return;
    }
    final double rotation = sample.barrelRotation ?? sample.orientation ?? 0;
    canvas
      ..save()
      ..translate(sample.localPosition.dx, sample.localPosition.dy)
      ..rotate(rotation)
      ..drawOval(
        Rect.fromCenter(center: Offset.zero, width: 28, height: 8),
        Paint()..color = const Color(0xffffd8e4),
      )
      ..restore();
  }

  @override
  bool shouldRepaint(covariant _StylusTrailPainter oldDelegate) =>
      oldDelegate.motion != motion || oldDelegate.points != points;
}

/// Formats an optional normalized value for telemetry.
String _formatNumber(double? value) =>
    value == null ? '—' : value.toStringAsFixed(3);

/// Formats an optional radian angle as degrees for telemetry.
String _formatDegrees(double? radians) =>
    radians == null ? '—' : '${(radians * 180 / math.pi).toStringAsFixed(1)}°';

/// Formats an optional native device change for compact telemetry.
String _formatDeviceEvent(StylusDeviceEvent? event) {
  if (event == null) {
    return '—';
  }
  final String label = event.device.name ?? event.device.identifier;
  return '${event.phase.name} · $label';
}

/// Formats an optional graphics-tablet pad event for compact telemetry.
String _formatPadEvent(TabletPadEvent? event) {
  if (event == null) {
    return '—';
  }
  final double? rawValue = event.value;
  final String value = rawValue == null
      ? ''
      : ' · ${rawValue.toStringAsFixed(3)}';
  return '${event.control.name}[${event.controlIndex}] · ${event.phase.name}$value';
}
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