diff --git a/CHANGELOG.md b/CHANGELOG.md index 545cbfe..96451d4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,6 @@ # 0.22.0 - feat: adding `NesIcons.shovel`. + - feat: adding `NesDpad` # 0.21.0 - Update to Flutter 3.22.0 diff --git a/example/lib/gallery/gallery_page.dart b/example/lib/gallery/gallery_page.dart index 4491e9b..3967bc7 100644 --- a/example/lib/gallery/gallery_page.dart +++ b/example/lib/gallery/gallery_page.dart @@ -39,6 +39,8 @@ class GalleryPage extends StatelessWidget { const SizedBox(height: 32), const RunningTextSection(), const SizedBox(height: 32), + const DpadsSection(), + const SizedBox(height: 32), const EffectsSection(), const SizedBox(height: 32), const LoadingIndicatorsSection(), diff --git a/example/lib/gallery/sections/dpads.dart b/example/lib/gallery/sections/dpads.dart new file mode 100644 index 0000000..e05f533 --- /dev/null +++ b/example/lib/gallery/sections/dpads.dart @@ -0,0 +1,60 @@ +import 'package:flutter/material.dart'; +import 'package:nes_ui/nes_ui.dart'; + +class DpadsSection extends StatelessWidget { + const DpadsSection({super.key}); + + @override + Widget build(BuildContext context) { + final theme = Theme.of(context); + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + 'Dpads', + style: theme.textTheme.displayMedium, + ), + const SizedBox(height: 16), + const ControllingDpad(), + ], + ); + } +} + +class ControllingDpad extends StatefulWidget { + const ControllingDpad({super.key}); + + @override + State createState() => _ControllingDpadState(); +} + +class _ControllingDpadState extends State { + NesDpadDirection? _direction; + + @override + Widget build(BuildContext context) { + return Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + NesDpad( + onButtonDown: (direction) { + setState(() { + _direction = direction; + }); + }, + onButtonUp: (direction) { + setState(() { + _direction = null; + }); + }, + ), + const SizedBox(height: 16), + Text( + 'Direction: ${_direction?.name ?? 'none'}', + ), + ], + ), + ); + } +} diff --git a/example/lib/gallery/sections/sections.dart b/example/lib/gallery/sections/sections.dart index 9946be5..4d4d1db 100644 --- a/example/lib/gallery/sections/sections.dart +++ b/example/lib/gallery/sections/sections.dart @@ -4,6 +4,7 @@ export 'checkboxes.dart'; export 'containers.dart'; export 'custom_extensions.dart'; export 'dialogs.dart'; +export 'dpads.dart'; export 'drop_shadows.dart'; export 'dropdown_menus.dart'; export 'effects.dart'; diff --git a/lib/src/widgets/nes_dpad.dart b/lib/src/widgets/nes_dpad.dart new file mode 100644 index 0000000..dfdfaa7 --- /dev/null +++ b/lib/src/widgets/nes_dpad.dart @@ -0,0 +1,198 @@ +import 'package:flutter/material.dart'; +import 'package:nes_ui/nes_ui.dart'; + +/// The four ways direction of the NES Dpad. +enum NesDpadDirection { + /// Up + up, + + /// Down + down, + + /// Left + left, + + /// Right + right, +} + +/// {@template nes_dpad} +/// A widget that renders a four ways directional pad. +/// {@endtemplate} +class NesDpad extends StatefulWidget { + /// {@macro nes_dpad} + const NesDpad({ + this.onButtonDown, + this.onButtonUp, + this.buttonBuilder = _defaultButtonBuilder, + this.buttonSize = 40, + super.key, + }); + + /// The callback that is called when a direction is pressed. + final void Function(NesDpadDirection)? onButtonDown; + + /// The callback that is called when a direction is released. + final void Function(NesDpadDirection)? onButtonUp; + + /// The builder that is called to render the key. + final Widget Function( + BuildContext context, + NesDpadDirection direction, { + required double buttonSize, + required bool pressed, + }) buttonBuilder; + + /// The size of the button. + final double buttonSize; + + @override + State createState() => _NesDpadState(); +} + +class _NesDpadState extends State { + bool _upPressed = false; + bool _downPressed = false; + bool _leftPressed = false; + bool _rightPressed = false; + + @override + Widget build(BuildContext context) { + return Column( + mainAxisSize: MainAxisSize.min, + children: [ + Row( + mainAxisSize: MainAxisSize.min, + children: [ + SizedBox(width: widget.buttonSize), + GestureDetector( + child: widget.buttonBuilder( + context, + NesDpadDirection.up, + buttonSize: widget.buttonSize, + pressed: _upPressed, + ), + onTapDown: (_) { + widget.onButtonDown?.call(NesDpadDirection.up); + setState(() { + _upPressed = true; + }); + }, + onTapUp: (_) { + widget.onButtonUp?.call(NesDpadDirection.up); + setState(() { + _upPressed = false; + }); + }, + ), + SizedBox(width: widget.buttonSize), + ], + ), + Row( + mainAxisSize: MainAxisSize.min, + children: [ + GestureDetector( + child: widget.buttonBuilder( + context, + NesDpadDirection.left, + buttonSize: widget.buttonSize, + pressed: _leftPressed, + ), + onTapDown: (_) { + widget.onButtonDown?.call(NesDpadDirection.left); + setState(() { + _leftPressed = true; + }); + }, + onTapUp: (_) { + widget.onButtonUp?.call(NesDpadDirection.left); + setState(() { + _leftPressed = false; + }); + }, + ), + SizedBox(width: widget.buttonSize), + GestureDetector( + child: widget.buttonBuilder( + context, + NesDpadDirection.right, + buttonSize: widget.buttonSize, + pressed: _rightPressed, + ), + onTapDown: (_) { + widget.onButtonDown?.call(NesDpadDirection.right); + setState(() { + _rightPressed = true; + }); + }, + onTapUp: (_) { + widget.onButtonUp?.call(NesDpadDirection.right); + setState(() { + _rightPressed = false; + }); + }, + ), + ], + ), + Row( + mainAxisSize: MainAxisSize.min, + children: [ + SizedBox(width: widget.buttonSize), + GestureDetector( + child: widget.buttonBuilder( + context, + NesDpadDirection.down, + buttonSize: widget.buttonSize, + pressed: _downPressed, + ), + onTapDown: (_) { + widget.onButtonDown?.call(NesDpadDirection.down); + setState(() { + _downPressed = true; + }); + }, + onTapUp: (_) { + widget.onButtonUp?.call(NesDpadDirection.down); + setState(() { + _downPressed = false; + }); + }, + ), + SizedBox(width: widget.buttonSize), + ], + ), + ], + ); + } +} + +Widget _defaultButtonBuilder( + BuildContext context, + NesDpadDirection direction, { + required double buttonSize, + required bool pressed, +}) { + late final NesIconData iconData; + + switch (direction) { + case NesDpadDirection.up: + iconData = NesIcons.topArrowIndicator; + case NesDpadDirection.down: + iconData = NesIcons.bottomArrowIndicator; + case NesDpadDirection.left: + iconData = NesIcons.leftArrowIndicator; + case NesDpadDirection.right: + iconData = NesIcons.rightArrowIndicator; + } + + return SizedBox.square( + dimension: buttonSize, + child: Transform.translate( + offset: pressed ? const Offset(0, 4) : Offset.zero, + child: NesIcon( + iconData: iconData, + size: Size.square(buttonSize), + ), + ), + ); +} diff --git a/lib/src/widgets/widgets.dart b/lib/src/widgets/widgets.dart index d679b06..9c13e02 100644 --- a/lib/src/widgets/widgets.dart +++ b/lib/src/widgets/widgets.dart @@ -6,6 +6,7 @@ export 'nes_button.dart'; export 'nes_check_box.dart'; export 'nes_checkered_decoration.dart'; export 'nes_controller_focus.dart'; +export 'nes_dpad.dart'; export 'nes_dropdown_menu.dart'; export 'nes_dropshadow.dart'; export 'nes_file_explorer.dart'; diff --git a/test/src/widgets/nes_dpad_test.dart b/test/src/widgets/nes_dpad_test.dart new file mode 100644 index 0000000..96d15fa --- /dev/null +++ b/test/src/widgets/nes_dpad_test.dart @@ -0,0 +1,22 @@ +// ignore_for_file: prefer_const_constructors + +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:nes_ui/nes_ui.dart'; + +void main() { + group('NesContainer', () { + testWidgets('renders its child', (tester) async { + await tester.pumpWidget( + MaterialApp( + theme: flutterNesTheme(), + home: const NesDpad(), + ), + ); + + expect(find.byType(NesDpad), findsOneWidget); + // By default the dpad has 4 icons + expect(find.byType(NesIcon), findsNWidgets(4)); + }); + }); +} diff --git a/widgetbook/lib/widgetbook/use_cases/dpads.dart b/widgetbook/lib/widgetbook/use_cases/dpads.dart new file mode 100644 index 0000000..89d5c89 --- /dev/null +++ b/widgetbook/lib/widgetbook/use_cases/dpads.dart @@ -0,0 +1,63 @@ +// ignore_for_file: public_member_api_docs + +import 'package:flutter/widgets.dart'; +import 'package:nes_ui/nes_ui.dart'; +import 'package:widgetbook_annotation/widgetbook_annotation.dart' as widgetbook; + +@widgetbook.UseCase( + name: 'default', + type: NesDpad, +) +Widget normal(BuildContext context) { + return const Center( + child: NesDpad(), + ); +} + +@widgetbook.UseCase( + name: 'with callbacks', + type: NesDpad, +) +Widget withCallbacks(BuildContext context) { + return const Center( + child: ControllingDpad(), + ); +} + +class ControllingDpad extends StatefulWidget { + const ControllingDpad({super.key}); + + @override + State createState() => _ControllingDpadState(); +} + +class _ControllingDpadState extends State { + NesDpadDirection? _direction; + + @override + Widget build(BuildContext context) { + return Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + NesDpad( + onButtonDown: (direction) { + setState(() { + _direction = direction; + }); + }, + onButtonUp: (direction) { + setState(() { + _direction = null; + }); + }, + ), + const SizedBox(height: 16), + Text( + 'Direction: ${_direction?.name ?? 'none'}', + ), + ], + ), + ); + } +} diff --git a/widgetbook/lib/widgetbook/widgetbook.directories.g.dart b/widgetbook/lib/widgetbook/widgetbook.directories.g.dart index b628682..a329de7 100644 --- a/widgetbook/lib/widgetbook/widgetbook.directories.g.dart +++ b/widgetbook/lib/widgetbook/widgetbook.directories.g.dart @@ -12,10 +12,11 @@ import 'package:widgetbook/widgetbook.dart' as _i1; import 'package:widgetbook_app/widgetbook/use_cases/buttons.dart' as _i2; import 'package:widgetbook_app/widgetbook/use_cases/checkboxes.dart' as _i3; -import 'package:widgetbook_app/widgetbook/use_cases/containers.dart' as _i7; -import 'package:widgetbook_app/widgetbook/use_cases/dropdown_menus.dart' as _i4; -import 'package:widgetbook_app/widgetbook/use_cases/running_texts.dart' as _i5; -import 'package:widgetbook_app/widgetbook/use_cases/typography.dart' as _i6; +import 'package:widgetbook_app/widgetbook/use_cases/containers.dart' as _i8; +import 'package:widgetbook_app/widgetbook/use_cases/dpads.dart' as _i4; +import 'package:widgetbook_app/widgetbook/use_cases/dropdown_menus.dart' as _i5; +import 'package:widgetbook_app/widgetbook/use_cases/running_texts.dart' as _i6; +import 'package:widgetbook_app/widgetbook/use_cases/typography.dart' as _i7; final directories = <_i1.WidgetbookNode>[ _i1.WidgetbookFolder( @@ -54,15 +55,28 @@ final directories = <_i1.WidgetbookNode>[ ), ), _i1.WidgetbookComponent( - name: 'NesDropdownMenu', + name: 'NesDpad', useCases: [ _i1.WidgetbookUseCase( name: 'default', builder: _i4.normal, ), + _i1.WidgetbookUseCase( + name: 'with callbacks', + builder: _i4.withCallbacks, + ), + ], + ), + _i1.WidgetbookComponent( + name: 'NesDropdownMenu', + useCases: [ + _i1.WidgetbookUseCase( + name: 'default', + builder: _i5.normal, + ), _i1.WidgetbookUseCase( name: 'with icons', - builder: _i4.icons, + builder: _i5.icons, ), ], ), @@ -70,14 +84,14 @@ final directories = <_i1.WidgetbookNode>[ name: 'NesRunningText', useCase: _i1.WidgetbookUseCase( name: 'default', - builder: _i5.normal, + builder: _i6.normal, ), ), _i1.WidgetbookLeafComponent( name: 'NesRunningTextLines', useCase: _i1.WidgetbookUseCase( name: 'default', - builder: _i5.lines, + builder: _i6.lines, ), ), _i1.WidgetbookComponent( @@ -85,23 +99,23 @@ final directories = <_i1.WidgetbookNode>[ useCases: [ _i1.WidgetbookUseCase( name: 'body', - builder: _i6.body, + builder: _i7.body, ), _i1.WidgetbookUseCase( name: 'display', - builder: _i6.display, + builder: _i7.display, ), _i1.WidgetbookUseCase( name: 'headline', - builder: _i6.headline, + builder: _i7.headline, ), _i1.WidgetbookUseCase( name: 'label', - builder: _i6.label, + builder: _i7.label, ), _i1.WidgetbookUseCase( name: 'title', - builder: _i6.title, + builder: _i7.title, ), ], ), @@ -113,19 +127,19 @@ final directories = <_i1.WidgetbookNode>[ useCases: [ _i1.WidgetbookUseCase( name: 'default', - builder: _i7.normal, + builder: _i8.normal, ), _i1.WidgetbookUseCase( name: 'with corner inner square painter', - builder: _i7.cornerInnerSquare, + builder: _i8.cornerInnerSquare, ), _i1.WidgetbookUseCase( name: 'with label', - builder: _i7.label, + builder: _i8.label, ), _i1.WidgetbookUseCase( name: 'with square corner painter', - builder: _i7.squareConer, + builder: _i8.squareConer, ), ], )