just_gamepads 0.1.0 copy "just_gamepads: ^0.1.0" to clipboard
just_gamepads: ^0.1.0 copied to clipboard

Gamepad input for Flutter on Android, iOS, macOS, Windows, Linux and the web (WASM too): Xbox, PlayStation, Nintendo and generic controllers in one standard layout.

example/lib/main.dart

import 'dart:async';

import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:just_gamepads/just_gamepads.dart';

void main() => runApp(const GamepadTesterApp());

/// Lists every connected controller with its live buttons and axes.
///
/// Also a hardware checklist: press each button and push each stick, and the
/// matching cell lights up in the standard layout.
class GamepadTesterApp extends StatelessWidget {
  const GamepadTesterApp({super.key});

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'just_gamepads tester',
      theme: ThemeData(colorSchemeSeed: Colors.indigo, useMaterial3: true),
      darkTheme: ThemeData(
        colorSchemeSeed: Colors.indigo,
        brightness: Brightness.dark,
        useMaterial3: true,
      ),
      home: const GamepadTesterPage(),
    );
  }
}

class GamepadTesterPage extends StatefulWidget {
  const GamepadTesterPage({super.key});

  @override
  State<GamepadTesterPage> createState() => _GamepadTesterPageState();
}

class _GamepadTesterPageState extends State<GamepadTesterPage> {
  final _manager = GamepadManager.instance;
  final _subscriptions = <StreamSubscription<Object>>[];

  @override
  void initState() {
    super.initState();
    _subscriptions
      ..add(
        _manager.connections.listen((event) {
          // Printed so the example doubles as a log-based smoke test.
          debugPrint('just_gamepads: $event');
          setState(() {});
        }),
      )
      ..add(_manager.stateChanges.listen((_) => setState(() {})));
    _manager.start().then((_) {
      final platform = kIsWasm
          ? 'web, wasm'
          : kIsWeb
          ? 'web, js'
          : defaultTargetPlatform.name;
      debugPrint(
        'just_gamepads: started ($platform), '
        '${_manager.connected.length} connected',
      );
      if (mounted) setState(() {});
    });
  }

  @override
  void dispose() {
    for (final s in _subscriptions) {
      s.cancel();
    }
    _manager.dispose();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    final pads = _manager.connected;
    return Scaffold(
      appBar: AppBar(title: const Text('just_gamepads tester')),
      body: pads.isEmpty
          ? const Center(
              child: Padding(
                padding: EdgeInsets.all(24),
                child: Text(
                  'No controller connected.\n\n'
                  'Connect one and press a button. Browsers only expose a '
                  'controller after a button press while the page has focus.',
                  textAlign: TextAlign.center,
                ),
              ),
            )
          : ListView(
              padding: const EdgeInsets.all(16),
              children: [
                for (final info in pads)
                  _PadCard(
                    info: info,
                    state: _manager.latestState(info.id) ?? GamepadState.empty,
                    isActive: _manager.mostRecentlyActiveId == info.id,
                  ),
              ],
            ),
    );
  }
}

class _PadCard extends StatelessWidget {
  const _PadCard({
    required this.info,
    required this.state,
    required this.isActive,
  });

  final GamepadInfo info;
  final GamepadState state;
  final bool isActive;

  static const _buttonNames = {
    GamepadButton.a: 'A',
    GamepadButton.b: 'B',
    GamepadButton.x: 'X',
    GamepadButton.y: 'Y',
    GamepadButton.leftBumper: 'LB',
    GamepadButton.rightBumper: 'RB',
    GamepadButton.leftTriggerButton: 'LT',
    GamepadButton.rightTriggerButton: 'RT',
    GamepadButton.back: 'Back',
    GamepadButton.start: 'Start',
    GamepadButton.leftStick: 'LS',
    GamepadButton.rightStick: 'RS',
    GamepadButton.dpadUp: 'Up',
    GamepadButton.dpadDown: 'Down',
    GamepadButton.dpadLeft: 'Left',
    GamepadButton.dpadRight: 'Right',
  };

  String _hex(int? value) => value == null
      ? '-'
      : '0x${value.toRadixString(16).padLeft(4, '0').toUpperCase()}';

  @override
  Widget build(BuildContext context) {
    final scheme = Theme.of(context).colorScheme;
    final text = Theme.of(context).textTheme;
    return Card(
      shape: RoundedRectangleBorder(
        borderRadius: BorderRadius.circular(12),
        side: BorderSide(
          color: isActive ? scheme.primary : scheme.outlineVariant,
          width: isActive ? 2 : 1,
        ),
      ),
      child: Padding(
        padding: const EdgeInsets.all(16),
        child: Column(
          crossAxisAlignment: CrossAxisAlignment.start,
          children: [
            Text(info.name, style: text.titleMedium),
            const SizedBox(height: 4),
            Text(
              '${info.type.name} · vendor ${_hex(info.vendorId)} · '
              'product ${_hex(info.productId)} · id ${info.id}'
              '${isActive ? ' · active' : ''}',
              style: text.bodySmall,
            ),
            const SizedBox(height: 12),
            Wrap(
              spacing: 6,
              runSpacing: 6,
              children: [
                for (final MapEntry(:key, :value) in _buttonNames.entries)
                  _ButtonChip(
                    label: value,
                    down: state.buttonsDown.contains(key),
                  ),
              ],
            ),
            const SizedBox(height: 12),
            for (final axis in GamepadAxis.values)
              _AxisBar(name: axis, value: state.axes[axis]),
          ],
        ),
      ),
    );
  }
}

class _ButtonChip extends StatelessWidget {
  const _ButtonChip({required this.label, required this.down});

  final String label;
  final bool down;

  @override
  Widget build(BuildContext context) {
    final scheme = Theme.of(context).colorScheme;
    return Container(
      constraints: const BoxConstraints(minWidth: 44),
      padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 6),
      decoration: BoxDecoration(
        color: down ? scheme.primary : scheme.surfaceContainerHighest,
        borderRadius: BorderRadius.circular(8),
      ),
      child: Text(
        label,
        textAlign: TextAlign.center,
        style: TextStyle(
          color: down ? scheme.onPrimary : scheme.onSurfaceVariant,
          fontWeight: FontWeight.w600,
        ),
      ),
    );
  }
}

class _AxisBar extends StatelessWidget {
  const _AxisBar({required this.name, required this.value});

  final String name;
  final double? value;

  @override
  Widget build(BuildContext context) {
    final scheme = Theme.of(context).colorScheme;
    final isTrigger = name.endsWith('Trigger');
    final v = value ?? 0.0;
    return Padding(
      padding: const EdgeInsets.symmetric(vertical: 3),
      child: Row(
        children: [
          SizedBox(width: 110, child: Text(name)),
          Expanded(
            child: ClipRRect(
              borderRadius: BorderRadius.circular(4),
              child: SizedBox(
                height: 10,
                child: CustomPaint(
                  painter: _AxisPainter(
                    value: v,
                    signed: !isTrigger,
                    track: scheme.surfaceContainerHighest,
                    fill: scheme.primary,
                  ),
                ),
              ),
            ),
          ),
          SizedBox(
            width: 64,
            child: Text(
              value == null ? 'n/a' : v.toStringAsFixed(2),
              textAlign: TextAlign.end,
              style: const TextStyle(
                fontFeatures: [FontFeature.tabularFigures()],
              ),
            ),
          ),
        ],
      ),
    );
  }
}

class _AxisPainter extends CustomPainter {
  _AxisPainter({
    required this.value,
    required this.signed,
    required this.track,
    required this.fill,
  });

  final double value;
  final bool signed;
  final Color track;
  final Color fill;

  @override
  void paint(Canvas canvas, Size size) {
    canvas.drawRect(Offset.zero & size, Paint()..color = track);
    final paint = Paint()..color = fill;
    if (signed) {
      final mid = size.width / 2;
      final end = mid + value.clamp(-1.0, 1.0) * mid;
      canvas.drawRect(
        Rect.fromLTRB(
          end < mid ? end : mid,
          0,
          end < mid ? mid : end,
          size.height,
        ),
        paint,
      );
    } else {
      canvas.drawRect(
        Rect.fromLTWH(0, 0, size.width * value.clamp(0.0, 1.0), size.height),
        paint,
      );
    }
  }

  @override
  bool shouldRepaint(_AxisPainter old) =>
      old.value != value || old.fill != fill || old.track != track;
}
0
likes
160
points
85
downloads

Documentation

API reference

Publisher

verified publisherjustunknown.com

Weekly Downloads

Gamepad input for Flutter on Android, iOS, macOS, Windows, Linux and the web (WASM too): Xbox, PlayStation, Nintendo and generic controllers in one standard layout.

Homepage
Repository (GitHub)
View/report issues
Contributing

Topics

#gamepad #controller #input #gamedev #game

License

BSD-3-Clause (license)

Dependencies

flutter, flutter_web_plugins, web

More

Packages that depend on just_gamepads

Packages that implement just_gamepads