Moved all widgets and logic from gui app to Flutter package

- Implemented DebugToolsScreen for navigation to asset galleries.
- Created GameScreen to manage gameplay and renderer integrations.
- Added NoGameDataScreen to handle scenarios with missing game data.
- Developed SpriteGallery for visual browsing of sprite assets.
- Introduced VgaGallery for displaying VGA images from game data.
- Added GalleryGameSelector widget for selecting game variants in galleries.
- Created Wolf3dApp as the main application shell for managing game states.
- Implemented WolfMenuShell for consistent menu layouts across screens.
- Enhanced Wolf3d class to support debug mode and related functionalities.
- Updated pubspec.yaml to include window_manager dependency.
- Added tests for game screen lifecycle and debug mode functionalities.

Signed-off-by: Hans Kokx <hans.d.kokx@gmail.com>
This commit is contained in:
2026-03-23 18:44:32 +01:00
parent cbe2633ceb
commit 5a2681e89b
21 changed files with 963 additions and 590 deletions
@@ -0,0 +1,253 @@
/// Active gameplay screen for the Flutter host.
library;
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:wolf_3d_dart/wolf_3d_engine.dart';
import 'package:wolf_3d_dart/wolf_3d_renderer.dart';
import 'package:wolf_3d_flutter/renderer/wolf_3d_ascii_renderer.dart';
import 'package:wolf_3d_flutter/renderer/wolf_3d_flutter_renderer.dart';
import 'package:wolf_3d_flutter/renderer/wolf_3d_glsl_renderer.dart';
import 'package:wolf_3d_flutter/wolf_3d_flutter.dart';
/// Launches a [WolfEngine] via [Wolf3d] and exposes renderer/input integrations.
class GameScreen extends StatefulWidget {
/// Shared application facade owning the engine, audio, and input.
final Wolf3d wolf3d;
/// Optional host-level shortcut override.
///
/// Return `true` when the event was consumed. Handlers may call
/// [Wolf3dFlutterInput.suppressActionOnce] to keep actions from reaching the
/// engine update loop.
final HostShortcutHandler? hostShortcutHandler;
/// Declarative host shortcut registry used when [hostShortcutHandler] is null.
final HostShortcutRegistry hostShortcutRegistry;
/// Optional host app-exit hook.
///
/// Defaults to [SystemNavigator.pop] in production, but tests can inject a
/// fake callback to assert quit behavior.
final Future<void> Function()? appExitHandler;
/// Skips engine bootstrap for lightweight widget tests.
@visibleForTesting
final bool skipEngineBootstrapForTest;
/// Creates a gameplay screen driven by [wolf3d].
const GameScreen({
required this.wolf3d,
this.hostShortcutHandler,
this.hostShortcutRegistry = HostShortcutRegistry.defaults,
this.appExitHandler,
this.skipEngineBootstrapForTest = false,
super.key,
});
@override
State<GameScreen> createState() => _GameScreenState();
}
class _GameScreenState extends State<GameScreen> {
WolfEngine? _engine;
late final GameAppLifecycleManager _appLifecycleManager;
late final GameDisplayManager _displayManager;
late final GameScreenInputManager _inputManager;
late final GamePersistenceManager _persistenceManager;
/// Mirrors [WolfRendererSettings.mode] into the Flutter renderer enum.
GameRendererMode _rendererMode = GameRendererMode.hardware;
@override
void initState() {
super.initState();
_appLifecycleManager = GameAppLifecycleManager(
shutdownAudio: widget.wolf3d.shutdownAudio,
appExitHandler: widget.appExitHandler,
);
_displayManager = GameDisplayManager();
_persistenceManager = GamePersistenceManager();
_inputManager = GameScreenInputManager(
input: widget.wolf3d.input,
hostShortcutHandler: widget.hostShortcutHandler,
hostShortcutRegistry: widget.hostShortcutRegistry,
onToggleFullscreen: _displayManager.toggleFullscreen,
);
if (widget.skipEngineBootstrapForTest) {
return;
}
const Set<WolfRendererMode> supportedModes = <WolfRendererMode>{
WolfRendererMode.hardware,
WolfRendererMode.software,
WolfRendererMode.ascii,
};
final engine = widget.wolf3d.launchEngine(
rendererCapabilities: const WolfRendererCapabilities(
supportedModes: supportedModes,
supportsAsciiThemes: true,
supportsHardwareEffects: true,
supportsBloom: true,
supportsFpsCounter: true,
),
rendererSettings: const WolfRendererSettings(
mode: WolfRendererMode.hardware,
),
onRendererSettingsChanged: (settings) {
unawaited(_persistenceManager.saveRendererSettings(settings));
if (mounted) {
setState(() {
_rendererMode = gameRendererModeFromSettings(settings);
});
}
},
onGameWon: () {
_engine?.difficulty = null;
widget.wolf3d.clearActiveDifficulty();
Navigator.of(context).pop();
},
onQuit: () {
unawaited(_appLifecycleManager.quitApplication());
},
saveGamePersistence: _persistenceManager.saveGamePersistence,
);
_engine = engine;
_rendererMode = gameRendererModeFromSettings(engine.rendererSettings);
unawaited(_persistenceManager.restoreRendererSettings(engine));
}
@override
void dispose() {
unawaited(widget.wolf3d.shutdownAudio());
super.dispose();
}
@override
Widget build(BuildContext context) {
final engine = _engine;
if (engine == null) {
return const Scaffold(body: SizedBox.shrink());
}
final WolfRendererSettings settings = engine.rendererSettings;
final Widget renderer = switch (_rendererMode) {
GameRendererMode.software => WolfFlutterRenderer(
engine: engine,
onKeyEvent: _handleRendererKeyEvent,
),
GameRendererMode.ascii => WolfAsciiRenderer(
engine: engine,
theme: settings.asciiThemeId == WolfRendererSettings.asciiThemeQuadrant
? AsciiThemes.quadrant
: AsciiThemes.blocks,
onKeyEvent: _handleRendererKeyEvent,
),
GameRendererMode.hardware => WolfGlslRenderer(
engine: engine,
effectsEnabled: settings.hardwareEffectsEnabled,
bloomEnabled: settings.bloomEnabled,
onKeyEvent: _handleRendererKeyEvent,
onUnavailable: _handleGlslUnavailable,
),
};
return PopScope(
canPop: engine.difficulty != null,
onPopInvokedWithResult: (didPop, _) {
if (!didPop && engine.difficulty == null) {
widget.wolf3d.input.queueBackAction();
}
},
child: Scaffold(
floatingActionButton:
widget.wolf3d.isDebugEnabled && engine.difficulty != null
? FloatingActionButton(
onPressed: _openDebugTools,
tooltip: 'Open Debug Tools',
child: const Icon(Icons.bug_report),
)
: null,
body: LayoutBuilder(
builder: (context, constraints) {
return Listener(
onPointerDown: (event) {
widget.wolf3d.input.onPointerDown(event);
},
onPointerUp: widget.wolf3d.input.onPointerUp,
onPointerMove: widget.wolf3d.input.onPointerMove,
onPointerHover: widget.wolf3d.input.onPointerMove,
child: Stack(
children: [
renderer,
if (!engine.isInitialized)
Container(
color: Colors.black,
child: const Center(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
CircularProgressIndicator(color: Colors.teal),
SizedBox(height: 20),
Text(
'GET PSYCHED!',
style: TextStyle(
color: Colors.teal,
fontFamily: 'monospace',
),
),
],
),
),
),
// A second full-screen overlay keeps the presentation simple while
// the engine is still warming up or decoding the first frame.
if (!engine.isInitialized)
Container(
color: Colors.black,
child: const Center(
child: CircularProgressIndicator(color: Colors.teal),
),
),
],
),
);
},
),
),
);
}
void _handleRendererKeyEvent(KeyEvent event) {
_inputManager.handleRendererKeyEventForHost(
event: event,
engine: _engine,
rendererMode: _rendererMode,
onFpsToggled: (WolfEngine activeEngine) {
setState(() => activeEngine.toggleFpsCounter());
},
);
}
void _handleGlslUnavailable() {
handleGlslUnavailable(
isMounted: mounted,
rendererMode: _rendererMode,
engine: _engine,
);
}
void _openDebugTools() {
Navigator.of(context).push(
MaterialPageRoute(
builder: (_) => DebugToolsScreen(wolf3d: widget.wolf3d),
),
);
}
}