feat: Enhance menu rendering and input handling
- Added support for new plugins: IrondashEngineContext and SuperNativeExtensions in the Flutter plugin registrant. - Updated CMake configuration to include new plugins. - Introduced a new dependency, super_clipboard, in pubspec.yaml. - Enhanced the WolfEngine to set the menu background color. - Implemented keyboard shortcuts for renderer mode toggling and ASCII theme cycling in CLI input handling. - Updated menu manager to include a universal menu background color. - Refactored ASCII and Sixel renderers to utilize the new menu background color and improved header drawing logic. - Simplified the drawing of menu options sidebars and header bars across different renderers. - Improved the layout and centering of menu titles in the header bar. Signed-off-by: Hans Kokx <hans.d.kokx@gmail.com>
This commit is contained in:
66
apps/wolf_3d_gui/lib/screens/debug_tools_screen.dart
Normal file
66
apps/wolf_3d_gui/lib/screens/debug_tools_screen.dart
Normal file
@@ -0,0 +1,66 @@
|
||||
/// Debug tools launcher for art and asset inspection screens.
|
||||
library;
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:wolf_3d_flutter/wolf_3d_flutter.dart';
|
||||
import 'package:wolf_3d_gui/screens/sprite_gallery.dart';
|
||||
import 'package:wolf_3d_gui/screens/vga_gallery.dart';
|
||||
|
||||
/// Presents debug-only navigation shortcuts for asset galleries.
|
||||
class DebugToolsScreen extends StatelessWidget {
|
||||
/// Shared app facade used to access active game assets.
|
||||
final Wolf3d wolf3d;
|
||||
|
||||
/// Creates the debug tools screen for [wolf3d].
|
||||
const DebugToolsScreen({super.key, required this.wolf3d});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: const Text('Debug Tools'),
|
||||
automaticallyImplyLeading: true,
|
||||
),
|
||||
body: ListView(
|
||||
padding: const EdgeInsets.all(16),
|
||||
children: [
|
||||
const Text(
|
||||
'Asset Galleries',
|
||||
style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
Card(
|
||||
child: ListTile(
|
||||
leading: const Icon(Icons.image_search),
|
||||
title: const Text('Sprite Gallery'),
|
||||
subtitle: const Text('Browse decoded sprite frames.'),
|
||||
trailing: const Icon(Icons.chevron_right),
|
||||
onTap: () {
|
||||
Navigator.of(context).push(
|
||||
MaterialPageRoute(
|
||||
builder: (_) => SpriteGallery(wolf3d: wolf3d),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
Card(
|
||||
child: ListTile(
|
||||
leading: const Icon(Icons.photo_library),
|
||||
title: const Text('VGA Gallery'),
|
||||
subtitle: const Text('Browse decoded VGA images.'),
|
||||
trailing: const Icon(Icons.chevron_right),
|
||||
onTap: () {
|
||||
Navigator.of(context).push(
|
||||
MaterialPageRoute(
|
||||
builder: (_) => VgaGallery(images: wolf3d.vgaImages),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,127 +0,0 @@
|
||||
/// Episode picker and asset-browser entry point for the selected game version.
|
||||
library;
|
||||
|
||||
import 'package:flutter/material.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/game_screen.dart';
|
||||
import 'package:wolf_3d_gui/screens/sprite_gallery.dart';
|
||||
import 'package:wolf_3d_gui/screens/vga_gallery.dart';
|
||||
|
||||
/// Presents the episode list and shortcuts into the asset gallery screens.
|
||||
class EpisodeScreen extends StatefulWidget {
|
||||
/// Shared application facade whose active game must already be set.
|
||||
final Wolf3d wolf3d;
|
||||
|
||||
/// Creates the episode-selection screen for [wolf3d].
|
||||
const EpisodeScreen({super.key, required this.wolf3d});
|
||||
|
||||
@override
|
||||
State<EpisodeScreen> createState() => _EpisodeScreenState();
|
||||
}
|
||||
|
||||
class _EpisodeScreenState extends State<EpisodeScreen> {
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
widget.wolf3d.audio.playMenuMusic();
|
||||
}
|
||||
|
||||
/// Persists the chosen episode and lets the engine present difficulty select.
|
||||
void _selectEpisode(int index) {
|
||||
widget.wolf3d.setActiveEpisode(index);
|
||||
widget.wolf3d.clearActiveDifficulty();
|
||||
Navigator.of(context).push(
|
||||
MaterialPageRoute(
|
||||
builder: (context) => GameScreen(wolf3d: widget.wolf3d),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final List<Episode> episodes = widget.wolf3d.activeGame.episodes;
|
||||
|
||||
return Scaffold(
|
||||
backgroundColor: Colors.teal,
|
||||
floatingActionButtonLocation:
|
||||
FloatingActionButtonLocation.miniCenterFloat,
|
||||
floatingActionButton: Row(
|
||||
children: [
|
||||
ElevatedButton(
|
||||
onPressed: () {
|
||||
Navigator.of(context).push(
|
||||
MaterialPageRoute(
|
||||
builder: (context) {
|
||||
return VgaGallery(images: widget.wolf3d.vgaImages);
|
||||
},
|
||||
),
|
||||
);
|
||||
},
|
||||
child: Text('VGA Gallery'),
|
||||
),
|
||||
ElevatedButton(
|
||||
onPressed: () {
|
||||
Navigator.of(context).push(
|
||||
MaterialPageRoute(
|
||||
builder: (context) {
|
||||
return SpriteGallery(
|
||||
wolf3d: widget.wolf3d,
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
},
|
||||
child: Text('Sprites'),
|
||||
),
|
||||
],
|
||||
),
|
||||
body: Center(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
const Text(
|
||||
'WHICH EPISODE TO PLAY?',
|
||||
style: TextStyle(
|
||||
color: Colors.red,
|
||||
fontSize: 32,
|
||||
fontWeight: FontWeight.bold,
|
||||
fontFamily: 'Courier',
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 40),
|
||||
ListView.builder(
|
||||
shrinkWrap: true,
|
||||
itemCount: episodes.length,
|
||||
itemBuilder: (context, index) {
|
||||
final Episode episode = episodes[index];
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
vertical: 8.0,
|
||||
horizontal: 32.0,
|
||||
),
|
||||
child: ElevatedButton(
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: Colors.blueGrey[900],
|
||||
foregroundColor: Colors.white,
|
||||
minimumSize: const Size(300, 60),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(4),
|
||||
),
|
||||
),
|
||||
onPressed: () => _selectEpisode(index),
|
||||
child: Text(
|
||||
episode.name,
|
||||
textAlign: TextAlign.center,
|
||||
style: const TextStyle(fontSize: 18),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -12,6 +12,7 @@ import 'package:wolf_3d_dart/wolf_3d_input.dart';
|
||||
import 'package:wolf_3d_dart/wolf_3d_renderer.dart';
|
||||
import 'package:wolf_3d_flutter/wolf_3d_flutter.dart';
|
||||
import 'package:wolf_3d_flutter/wolf_3d_input_flutter.dart';
|
||||
import 'package:wolf_3d_gui/screens/debug_tools_screen.dart';
|
||||
import 'package:wolf_3d_renderer/wolf_3d_ascii_renderer.dart';
|
||||
import 'package:wolf_3d_renderer/wolf_3d_flutter_renderer.dart';
|
||||
import 'package:wolf_3d_renderer/wolf_3d_glsl_renderer.dart';
|
||||
@@ -173,6 +174,11 @@ class _GameScreenState extends State<GameScreen> {
|
||||
}
|
||||
},
|
||||
child: Scaffold(
|
||||
floatingActionButton: FloatingActionButton(
|
||||
onPressed: _openDebugTools,
|
||||
tooltip: 'Open Debug Tools',
|
||||
child: const Icon(Icons.bug_report),
|
||||
),
|
||||
body: LayoutBuilder(
|
||||
builder: (context, constraints) {
|
||||
return Listener(
|
||||
@@ -336,6 +342,14 @@ class _GameScreenState extends State<GameScreen> {
|
||||
_glslEffectsEnabled = !_glslEffectsEnabled;
|
||||
}
|
||||
|
||||
void _openDebugTools() {
|
||||
Navigator.of(context).push(
|
||||
MaterialPageRoute(
|
||||
builder: (_) => DebugToolsScreen(wolf3d: widget.wolf3d),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
bool _handleHostShortcut(KeyEvent event) {
|
||||
final HostShortcutHandler? customHandler = widget.hostShortcutHandler;
|
||||
if (customHandler != null) {
|
||||
|
||||
@@ -62,9 +62,11 @@ class SpriteGallery extends StatelessWidget {
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
Expanded(
|
||||
child: AspectRatio(
|
||||
aspectRatio: 4 / 3,
|
||||
child: WolfAssetPainter.sprite(wolf3d.sprites[index]),
|
||||
child: Center(
|
||||
child: AspectRatio(
|
||||
aspectRatio: 1,
|
||||
child: WolfAssetPainter.sprite(wolf3d.sprites[index]),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
|
||||
@@ -1,7 +1,12 @@
|
||||
/// 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_renderer/wolf_3d_asset_painter.dart';
|
||||
|
||||
@@ -13,6 +18,110 @@ class VgaGallery extends StatelessWidget {
|
||||
/// Creates the gallery for [images].
|
||||
const VgaGallery({super.key, required this.images});
|
||||
|
||||
Future<ui.Image> _buildVgaUiImage(VgaImage image) {
|
||||
final completer = Completer<ui.Image>();
|
||||
final Uint8List pixels = Uint8List(image.width * image.height * 4);
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
ui.decodeImageFromPixels(
|
||||
pixels,
|
||||
image.width,
|
||||
image.height,
|
||||
ui.PixelFormat.rgba8888,
|
||||
(ui.Image decoded) => completer.complete(decoded),
|
||||
);
|
||||
return completer.future;
|
||||
}
|
||||
|
||||
Future<void> _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.')),
|
||||
);
|
||||
}
|
||||
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();
|
||||
}
|
||||
}
|
||||
|
||||
void _showImagePreviewDialog(BuildContext context, int index) {
|
||||
final VgaImage image = images[index];
|
||||
|
||||
showDialog<void>(
|
||||
context: context,
|
||||
builder: (dialogContext) {
|
||||
return AlertDialog(
|
||||
title: Text(
|
||||
'Index: $index ${image.width} x ${image.height}',
|
||||
style: const TextStyle(
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
actions: [
|
||||
TextButton(
|
||||
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(
|
||||
aspectRatio: image.width / image.height,
|
||||
child: WolfAssetPainter.vga(image),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
@@ -28,21 +137,27 @@ class VgaGallery extends StatelessWidget {
|
||||
itemBuilder: (context, index) {
|
||||
return Card(
|
||||
color: Colors.blueGrey,
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
spacing: 8,
|
||||
children: [
|
||||
Text(
|
||||
"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: WolfAssetPainter.vga(images[index]),
|
||||
child: InkWell(
|
||||
onTap: () => _showImagePreviewDialog(context, index),
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
spacing: 8,
|
||||
children: [
|
||||
Text(
|
||||
"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]),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
|
||||
@@ -7,16 +7,24 @@
|
||||
#include "generated_plugin_registrant.h"
|
||||
|
||||
#include <audioplayers_linux/audioplayers_linux_plugin.h>
|
||||
#include <irondash_engine_context/irondash_engine_context_plugin.h>
|
||||
#include <screen_retriever_linux/screen_retriever_linux_plugin.h>
|
||||
#include <super_native_extensions/super_native_extensions_plugin.h>
|
||||
#include <window_manager/window_manager_plugin.h>
|
||||
|
||||
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);
|
||||
|
||||
@@ -4,7 +4,9 @@
|
||||
|
||||
list(APPEND FLUTTER_PLUGIN_LIST
|
||||
audioplayers_linux
|
||||
irondash_engine_context
|
||||
screen_retriever_linux
|
||||
super_native_extensions
|
||||
window_manager
|
||||
)
|
||||
|
||||
|
||||
@@ -16,6 +16,7 @@ dependencies:
|
||||
|
||||
flutter:
|
||||
sdk: flutter
|
||||
super_clipboard: ^0.9.1
|
||||
|
||||
dev_dependencies:
|
||||
flutter_test:
|
||||
|
||||
Reference in New Issue
Block a user