From 03dd871a46d94323c3c0ea3c7b24a2493e108cf4 Mon Sep 17 00:00:00 2001 From: Hans Kokx Date: Fri, 20 Mar 2026 15:53:24 +0100 Subject: [PATCH] feat: Implement game selection in sprite and VGA galleries with GalleryGameSelector refactor: Update VgaGallery and SpriteGallery to use selected game data chore: Remove unused plugins from generated plugin registrant and CMake files chore: Clean up pubspec.yaml by removing super_clipboard dependency Signed-off-by: Hans Kokx --- .../lib/screens/debug_tools_screen.dart | 2 +- .../lib/screens/gallery_game_selector.dart | 57 +++++++ .../lib/screens/sprite_gallery.dart | 148 +++++++++++++----- apps/wolf_3d_gui/lib/screens/vga_gallery.dart | 122 +++++---------- .../flutter/generated_plugin_registrant.cc | 8 - .../linux/flutter/generated_plugins.cmake | 2 - apps/wolf_3d_gui/pubspec.yaml | 1 - 7 files changed, 212 insertions(+), 128 deletions(-) create mode 100644 apps/wolf_3d_gui/lib/screens/gallery_game_selector.dart diff --git a/apps/wolf_3d_gui/lib/screens/debug_tools_screen.dart b/apps/wolf_3d_gui/lib/screens/debug_tools_screen.dart index 01e9857..bc4b1be 100644 --- a/apps/wolf_3d_gui/lib/screens/debug_tools_screen.dart +++ b/apps/wolf_3d_gui/lib/screens/debug_tools_screen.dart @@ -53,7 +53,7 @@ class DebugToolsScreen extends StatelessWidget { onTap: () { Navigator.of(context).push( MaterialPageRoute( - builder: (_) => VgaGallery(images: wolf3d.vgaImages), + builder: (_) => VgaGallery(wolf3d: wolf3d), ), ); }, diff --git a/apps/wolf_3d_gui/lib/screens/gallery_game_selector.dart b/apps/wolf_3d_gui/lib/screens/gallery_game_selector.dart new file mode 100644 index 0000000..b4f80ab --- /dev/null +++ b/apps/wolf_3d_gui/lib/screens/gallery_game_selector.dart @@ -0,0 +1,57 @@ +import 'package:flutter/material.dart'; +import 'package:wolf_3d_dart/wolf_3d_data_types.dart'; +import 'package:wolf_3d_flutter/wolf_3d_flutter.dart'; + +String formatGalleryGameTitle(GameVersion version) { + switch (version) { + case GameVersion.shareware: + return 'SHAREWARE'; + case GameVersion.retail: + return 'RETAIL'; + case GameVersion.spearOfDestiny: + return 'SPEAR OF DESTINY'; + case GameVersion.spearOfDestinyDemo: + return 'SOD DEMO'; + } +} + +class GalleryGameSelector extends StatelessWidget { + final Wolf3d wolf3d; + final WolfensteinData selectedGame; + final ValueChanged onSelected; + + const GalleryGameSelector({ + super.key, + required this.wolf3d, + required this.selectedGame, + required this.onSelected, + }); + + @override + Widget build(BuildContext context) { + return Padding( + padding: const EdgeInsets.only(right: 12), + child: Center( + child: DropdownButtonHideUnderline( + child: DropdownButton( + value: selectedGame, + borderRadius: BorderRadius.circular(8), + dropdownColor: Theme.of(context).colorScheme.surface, + iconEnabledColor: Theme.of(context).colorScheme.onSurface, + onChanged: (WolfensteinData? game) { + if (game != null) { + onSelected(game); + } + }, + items: wolf3d.availableGames.map((WolfensteinData game) { + return DropdownMenuItem( + value: game, + child: Text(formatGalleryGameTitle(game.version)), + ); + }).toList(), + ), + ), + ), + ); + } +} diff --git a/apps/wolf_3d_gui/lib/screens/sprite_gallery.dart b/apps/wolf_3d_gui/lib/screens/sprite_gallery.dart index 2fbefc0..d9e6c38 100644 --- a/apps/wolf_3d_gui/lib/screens/sprite_gallery.dart +++ b/apps/wolf_3d_gui/lib/screens/sprite_gallery.dart @@ -5,24 +5,113 @@ import 'package:flutter/material.dart'; import 'package:wolf_3d_dart/wolf_3d_data_types.dart'; import 'package:wolf_3d_dart/wolf_3d_entities.dart'; import 'package:wolf_3d_flutter/wolf_3d_flutter.dart'; +import 'package:wolf_3d_gui/screens/gallery_game_selector.dart'; import 'package:wolf_3d_renderer/wolf_3d_asset_painter.dart'; /// Displays every sprite frame in the active game along with enemy metadata. -class SpriteGallery extends StatelessWidget { +class SpriteGallery extends StatefulWidget { /// Shared application facade used to access the active game's sprite set. final Wolf3d wolf3d; /// Creates the sprite gallery for [wolf3d]. const SpriteGallery({super.key, required this.wolf3d}); - bool get isShareware => wolf3d.activeGame.version == GameVersion.shareware; + @override + State createState() => _SpriteGalleryState(); +} + +class _SpriteGalleryState extends State { + late WolfensteinData _selectedGame; + + @override + void initState() { + super.initState(); + _selectedGame = + widget.wolf3d.maybeActiveGame ?? widget.wolf3d.availableGames.first; + } + + bool get isShareware => _selectedGame.version == GameVersion.shareware; + + List get _sprites => _selectedGame.sprites; + + void _selectGame(WolfensteinData game) { + if (identical(_selectedGame, game)) { + return; + } + + widget.wolf3d.setActiveGame(game); + setState(() { + _selectedGame = game; + }); + } + + String _buildSpriteLabel(int index) { + String label = 'Sprite Index: $index'; + for (final enemy in EnemyType.values) { + // The gallery infers likely ownership from sprite index ranges so + // debugging art packs does not require cross-referencing source. + if (enemy.claimsSpriteIndex(index, isShareware: isShareware)) { + final EnemyAnimation? animation = enemy.getAnimationFromSprite( + index, + isShareware: isShareware, + ); + + label += '\n${enemy.name}'; + if (animation != null) { + label += '\n${animation.name}'; + } + break; + } + } + + return label; + } + + void _showImagePreviewDialog(BuildContext context, int index) { + final Sprite sprite = _sprites[index]; + + showDialog( + context: context, + builder: (dialogContext) { + return AlertDialog( + title: Text( + '${_buildSpriteLabel(index)}\n64 x 64', + style: const TextStyle( + fontSize: 14, + fontWeight: FontWeight.w600, + ), + ), + actions: [ + TextButton( + onPressed: () => Navigator.of(dialogContext).pop(), + child: const Text('Close'), + ), + ], + content: Center( + child: AspectRatio( + aspectRatio: 1, + child: WolfAssetPainter.sprite(sprite), + ), + ), + ); + }, + ); + } @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( - title: const Text("Sprite Gallery"), + title: const Text('Sprite Gallery'), automaticallyImplyLeading: true, + actions: [ + if (widget.wolf3d.availableGames.length > 1) + GalleryGameSelector( + wolf3d: widget.wolf3d, + selectedGame: _selectedGame, + onSelected: _selectGame, + ), + ], ), backgroundColor: Colors.black, body: GridView.builder( @@ -31,45 +120,32 @@ class SpriteGallery extends StatelessWidget { crossAxisSpacing: 8, mainAxisSpacing: 8, ), - itemCount: wolf3d.sprites.length, + itemCount: _sprites.length, itemBuilder: (context, index) { - String label = "Sprite Index: $index"; - for (final enemy in EnemyType.values) { - // The gallery infers likely ownership from sprite index ranges so - // debugging art packs does not require cross-referencing source. - if (enemy.claimsSpriteIndex(index, isShareware: isShareware)) { - final EnemyAnimation? animation = enemy.getAnimationFromSprite( - index, - isShareware: isShareware, - ); - - label += "\n${enemy.name}"; - if (animation != null) { - label += "\n${animation.name}"; - } - break; - } - } + final String label = _buildSpriteLabel(index); return Card( color: Colors.blueGrey, - child: Column( - spacing: 8, - children: [ - Text( - label, - style: const TextStyle(color: Colors.white, fontSize: 10), - textAlign: TextAlign.center, - ), - Expanded( - child: Center( - child: AspectRatio( - aspectRatio: 1, - child: WolfAssetPainter.sprite(wolf3d.sprites[index]), + child: InkWell( + onTap: () => _showImagePreviewDialog(context, index), + child: Column( + spacing: 8, + children: [ + Text( + label, + style: const TextStyle(color: Colors.white, fontSize: 10), + textAlign: TextAlign.center, + ), + Expanded( + child: Center( + child: AspectRatio( + aspectRatio: 1, + child: WolfAssetPainter.sprite(_sprites[index]), + ), ), ), - ), - ], + ], + ), ), ); }, diff --git a/apps/wolf_3d_gui/lib/screens/vga_gallery.dart b/apps/wolf_3d_gui/lib/screens/vga_gallery.dart index 8347307..6c895b5 100644 --- a/apps/wolf_3d_gui/lib/screens/vga_gallery.dart +++ b/apps/wolf_3d_gui/lib/screens/vga_gallery.dart @@ -1,93 +1,49 @@ /// Visual browser for decoded VGA pictures and UI art. library; -import 'dart:async'; -import 'dart:typed_data'; -import 'dart:ui' as ui; - import 'package:flutter/material.dart'; -import 'package:super_clipboard/super_clipboard.dart'; import 'package:wolf_3d_dart/wolf_3d_data_types.dart'; +import 'package:wolf_3d_flutter/wolf_3d_flutter.dart'; +import 'package:wolf_3d_gui/screens/gallery_game_selector.dart'; import 'package:wolf_3d_renderer/wolf_3d_asset_painter.dart'; /// Shows each VGA image extracted from the currently selected game data set. -class VgaGallery extends StatelessWidget { - /// Raw VGA images decoded from the active asset pack. - final List images; +class VgaGallery extends StatefulWidget { + /// Shared app facade used to access available game data sets. + final Wolf3d wolf3d; - /// Creates the gallery for [images]. - const VgaGallery({super.key, required this.images}); + /// Creates the gallery for the currently selected or browsed game. + const VgaGallery({super.key, required this.wolf3d}); - Future _buildVgaUiImage(VgaImage image) { - final completer = Completer(); - final Uint8List pixels = Uint8List(image.width * image.height * 4); + @override + State createState() => _VgaGalleryState(); +} - for (int y = 0; y < image.height; y++) { - for (int x = 0; x < image.width; x++) { - final int colorByte = image.decodePixel(x, y); - final int abgr = ColorPalette.vga32Bit[colorByte]; - final int offset = (y * image.width + x) * 4; - pixels[offset] = abgr & 0xFF; // R - pixels[offset + 1] = (abgr >> 8) & 0xFF; // G - pixels[offset + 2] = (abgr >> 16) & 0xFF; // B - pixels[offset + 3] = (abgr >> 24) & 0xFF; // A - } - } +class _VgaGalleryState extends State { + late WolfensteinData _selectedGame; - ui.decodeImageFromPixels( - pixels, - image.width, - image.height, - ui.PixelFormat.rgba8888, - (ui.Image decoded) => completer.complete(decoded), - ); - return completer.future; + List get _images => _selectedGame.vgaImages; + + @override + void initState() { + super.initState(); + _selectedGame = + widget.wolf3d.maybeActiveGame ?? widget.wolf3d.availableGames.first; } - Future _copyImageToClipboard(BuildContext context, int index) async { - final clipboard = SystemClipboard.instance; - if (clipboard == null) { - if (context.mounted) { - ScaffoldMessenger.of(context).showSnackBar( - const SnackBar(content: Text('Clipboard API is unavailable here.')), - ); - } + void _selectGame(WolfensteinData game) { + if (identical(_selectedGame, game)) { return; } - final VgaImage image = images[index]; - ui.Image? uiImage; - try { - uiImage = await _buildVgaUiImage(image); - final ByteData? pngData = await uiImage.toByteData( - format: ui.ImageByteFormat.png, - ); - if (pngData == null) { - throw StateError('Failed to encode image as PNG.'); - } - - final item = DataWriterItem(); - item.add(Formats.png(pngData.buffer.asUint8List())); - await clipboard.write([item]); - - if (context.mounted) { - ScaffoldMessenger.of(context).showSnackBar( - SnackBar(content: Text('Copied VGA image #$index to clipboard.')), - ); - } - } catch (error) { - if (context.mounted) { - ScaffoldMessenger.of(context).showSnackBar( - SnackBar(content: Text('Copy failed: $error')), - ); - } - } finally { - uiImage?.dispose(); - } + widget.wolf3d.setActiveGame(game); + setState(() { + _selectedGame = game; + }); } void _showImagePreviewDialog(BuildContext context, int index) { - final VgaImage image = images[index]; + final VgaImage image = _images[index]; showDialog( context: context, @@ -105,11 +61,6 @@ class VgaGallery extends StatelessWidget { onPressed: () => Navigator.of(dialogContext).pop(), child: const Text('Close'), ), - ElevatedButton.icon( - onPressed: () => _copyImageToClipboard(dialogContext, index), - icon: const Icon(Icons.copy), - label: const Text('Copy'), - ), ], content: Center( child: AspectRatio( @@ -125,7 +76,17 @@ class VgaGallery extends StatelessWidget { @override Widget build(BuildContext context) { return Scaffold( - appBar: AppBar(title: const Text("VGA Image Gallery")), + appBar: AppBar( + title: const Text('VGA Image Gallery'), + actions: [ + if (widget.wolf3d.availableGames.length > 1) + GalleryGameSelector( + wolf3d: widget.wolf3d, + selectedGame: _selectedGame, + onSelected: _selectGame, + ), + ], + ), backgroundColor: Colors.black, body: GridView.builder( gridDelegate: const SliverGridDelegateWithMaxCrossAxisExtent( @@ -133,7 +94,7 @@ class VgaGallery extends StatelessWidget { crossAxisSpacing: 8, mainAxisSpacing: 8, ), - itemCount: images.length, + itemCount: _images.length, itemBuilder: (context, index) { return Card( color: Colors.blueGrey, @@ -144,15 +105,16 @@ class VgaGallery extends StatelessWidget { spacing: 8, children: [ Text( - "Index: $index\n${images[index].width} x ${images[index].height}", + 'Index: $index\n${_images[index].width} x ${_images[index].height}', style: const TextStyle(color: Colors.white, fontSize: 12), textAlign: TextAlign.center, ), Expanded( child: Center( child: AspectRatio( - aspectRatio: images[index].width / images[index].height, - child: WolfAssetPainter.vga(images[index]), + aspectRatio: + _images[index].width / _images[index].height, + child: WolfAssetPainter.vga(_images[index]), ), ), ), diff --git a/apps/wolf_3d_gui/linux/flutter/generated_plugin_registrant.cc b/apps/wolf_3d_gui/linux/flutter/generated_plugin_registrant.cc index b63cbe9..d7f5477 100644 --- a/apps/wolf_3d_gui/linux/flutter/generated_plugin_registrant.cc +++ b/apps/wolf_3d_gui/linux/flutter/generated_plugin_registrant.cc @@ -7,24 +7,16 @@ #include "generated_plugin_registrant.h" #include -#include #include -#include #include void fl_register_plugins(FlPluginRegistry* registry) { g_autoptr(FlPluginRegistrar) audioplayers_linux_registrar = fl_plugin_registry_get_registrar_for_plugin(registry, "AudioplayersLinuxPlugin"); audioplayers_linux_plugin_register_with_registrar(audioplayers_linux_registrar); - g_autoptr(FlPluginRegistrar) irondash_engine_context_registrar = - fl_plugin_registry_get_registrar_for_plugin(registry, "IrondashEngineContextPlugin"); - irondash_engine_context_plugin_register_with_registrar(irondash_engine_context_registrar); g_autoptr(FlPluginRegistrar) screen_retriever_linux_registrar = fl_plugin_registry_get_registrar_for_plugin(registry, "ScreenRetrieverLinuxPlugin"); screen_retriever_linux_plugin_register_with_registrar(screen_retriever_linux_registrar); - g_autoptr(FlPluginRegistrar) super_native_extensions_registrar = - fl_plugin_registry_get_registrar_for_plugin(registry, "SuperNativeExtensionsPlugin"); - super_native_extensions_plugin_register_with_registrar(super_native_extensions_registrar); g_autoptr(FlPluginRegistrar) window_manager_registrar = fl_plugin_registry_get_registrar_for_plugin(registry, "WindowManagerPlugin"); window_manager_plugin_register_with_registrar(window_manager_registrar); diff --git a/apps/wolf_3d_gui/linux/flutter/generated_plugins.cmake b/apps/wolf_3d_gui/linux/flutter/generated_plugins.cmake index 9ea952a..de49b37 100644 --- a/apps/wolf_3d_gui/linux/flutter/generated_plugins.cmake +++ b/apps/wolf_3d_gui/linux/flutter/generated_plugins.cmake @@ -4,9 +4,7 @@ list(APPEND FLUTTER_PLUGIN_LIST audioplayers_linux - irondash_engine_context screen_retriever_linux - super_native_extensions window_manager ) diff --git a/apps/wolf_3d_gui/pubspec.yaml b/apps/wolf_3d_gui/pubspec.yaml index fb59259..4e47cdf 100644 --- a/apps/wolf_3d_gui/pubspec.yaml +++ b/apps/wolf_3d_gui/pubspec.yaml @@ -16,7 +16,6 @@ dependencies: flutter: sdk: flutter - super_clipboard: ^0.9.1 dev_dependencies: flutter_test: