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,17 @@
library;
import 'package:flutter/foundation.dart';
/// Whether desktop window-management APIs are expected to work on this host.
bool get supportsDesktopWindowing {
if (kIsWeb) {
return false;
}
return switch (defaultTargetPlatform) {
TargetPlatform.linux ||
TargetPlatform.windows ||
TargetPlatform.macOS => true,
_ => false,
};
}
@@ -0,0 +1,42 @@
library;
import 'package:flutter/services.dart';
/// Coordinates host application shutdown for gameplay sessions.
class GameAppLifecycleManager {
/// Creates a lifecycle manager.
GameAppLifecycleManager({
required this.shutdownAudio,
this.appExitHandler,
});
/// Callback used to stop and dispose shared audio resources.
final Future<void> Function() shutdownAudio;
/// Optional host-specific app exit callback.
final Future<void> Function()? appExitHandler;
Future<void>? _quitFuture;
/// Shuts down audio and then exits the host app once.
Future<void> quitApplication() async {
final Future<void>? existing = _quitFuture;
if (existing != null) {
await existing;
return;
}
final Future<void> quit = () async {
await shutdownAudio();
final Future<void> Function()? handler = appExitHandler;
if (handler != null) {
await handler();
} else {
await SystemNavigator.pop();
}
}();
_quitFuture = quit;
await quit;
}
}
@@ -0,0 +1,36 @@
library;
import 'package:flutter/services.dart';
import 'package:window_manager/window_manager.dart';
import 'package:wolf_3d_flutter/managers/desktop_windowing_support.dart'
as support;
/// Handles host display/window operations for gameplay screens.
class GameDisplayManager {
/// Creates a display manager.
GameDisplayManager({WindowManager? windowing})
: windowing = windowing ?? windowManager;
/// Window manager instance used for desktop host operations.
final WindowManager windowing;
/// Toggles desktop fullscreen state when supported by the host platform.
///
/// This no-ops on unsupported targets and safely ignores missing plugin
/// hosts to keep gameplay input resilient in embedded/test environments.
Future<void> toggleFullscreen() async {
if (!supportsDesktopWindowing) {
return;
}
try {
final bool isFullScreen = await windowing.isFullScreen();
await windowing.setFullScreen(!isFullScreen);
} on MissingPluginException {
// No-op on hosts where the window manager plugin is unavailable.
}
}
/// Whether runtime desktop window management APIs are expected to work.
bool get supportsDesktopWindowing => support.supportsDesktopWindowing;
}
@@ -0,0 +1,42 @@
library;
import 'package:wolf_3d_dart/wolf_3d_engine.dart';
/// Coordinates gameplay persistence concerns for Flutter hosts.
class GamePersistenceManager {
/// Creates persistence manager dependencies with overridable adapters.
GamePersistenceManager({
RendererSettingsPersistence? rendererSettingsPersistence,
SaveGamePersistence? saveGamePersistence,
}) : rendererSettingsPersistence =
rendererSettingsPersistence ?? DefaultRendererSettingsPersistence(),
saveGamePersistence =
saveGamePersistence ?? DefaultSaveGamePersistence();
/// Persists and restores runtime renderer settings.
final RendererSettingsPersistence rendererSettingsPersistence;
/// Persists slot-based save game snapshots.
final SaveGamePersistence saveGamePersistence;
/// Loads previously persisted renderer settings.
Future<WolfRendererSettings?> loadRendererSettings() {
return rendererSettingsPersistence.load();
}
/// Loads persisted renderer settings and applies them to [engine].
Future<WolfRendererSettings?> restoreRendererSettings(
WolfEngine engine,
) async {
final WolfRendererSettings? saved = await loadRendererSettings();
if (saved != null) {
engine.updateRendererSettings(saved);
}
return saved;
}
/// Saves current renderer settings.
Future<void> saveRendererSettings(WolfRendererSettings settings) {
return rendererSettingsPersistence.save(settings);
}
}
@@ -0,0 +1,44 @@
library;
import 'package:wolf_3d_dart/wolf_3d_engine.dart';
/// Renderer presentation mode used by Flutter host widgets.
enum GameRendererMode {
/// Software pixel renderer presented via decoded framebuffer images.
software,
/// Text-mode renderer for debugging and retro terminal aesthetics.
ascii,
/// GLSL renderer with optional CRT-style post processing.
hardware,
}
/// Maps engine renderer settings to host renderer presentation mode.
GameRendererMode gameRendererModeFromSettings(WolfRendererSettings settings) {
return switch (settings.mode) {
WolfRendererMode.hardware => GameRendererMode.hardware,
WolfRendererMode.software => GameRendererMode.software,
WolfRendererMode.ascii || WolfRendererMode.sixel => GameRendererMode.ascii,
};
}
/// Falls back to software mode when GLSL rendering is unavailable at runtime.
void handleGlslUnavailable({
required bool isMounted,
required GameRendererMode rendererMode,
required WolfEngine? engine,
}) {
if (!isMounted || rendererMode != GameRendererMode.hardware) {
return;
}
final WolfEngine? activeEngine = engine;
if (activeEngine == null) {
return;
}
activeEngine.updateRendererSettings(
activeEngine.rendererSettings.copyWith(mode: WolfRendererMode.software),
);
}
@@ -0,0 +1,217 @@
library;
import 'dart:async';
import 'package:flutter/services.dart';
import 'package:wolf_3d_dart/wolf_3d_engine.dart';
import 'package:wolf_3d_dart/wolf_3d_input.dart';
import 'package:wolf_3d_flutter/managers/game_renderer_mode_manager.dart';
import 'package:wolf_3d_flutter/wolf_3d_input_flutter.dart';
/// Semantic actions that host-level shortcuts can trigger.
///
/// These intents are intentionally UI-host focused (windowing, app shell), not
/// engine gameplay actions. The engine continues to receive input through
/// [Wolf3dFlutterInput].
enum HostShortcutIntent {
/// Toggle desktop fullscreen on/off.
toggleFullscreen,
}
/// Declarative mapping from a key pattern to a host shortcut intent.
///
/// [matches] identifies whether a key event should trigger this binding.
/// [suppressedActions] are one-frame engine actions that should be blocked
/// when the binding is consumed (for example, blocking `interact` on Alt+Enter
/// so Enter does not activate menu selections).
class HostShortcutBinding {
/// Predicate that returns true when this shortcut should fire.
final bool Function(KeyEvent event) matches;
/// Host operation to perform when [matches] succeeds.
final HostShortcutIntent intent;
/// Engine actions to suppress for a single input update tick.
final Set<WolfInputAction> suppressedActions;
/// Creates a host shortcut binding with optional suppressed engine actions.
const HostShortcutBinding({
required this.matches,
required this.intent,
this.suppressedActions = const <WolfInputAction>{},
});
}
/// Ordered set of host shortcut bindings.
///
/// The first binding whose [HostShortcutBinding.matches] returns true wins.
/// This keeps behavior deterministic when multiple shortcuts could overlap.
class HostShortcutRegistry {
/// Ordered bindings consulted for each incoming key-down event.
final List<HostShortcutBinding> bindings;
/// Creates a registry with explicit [bindings].
const HostShortcutRegistry({
required this.bindings,
});
/// Returns the first binding that matches [event], or null when none do.
HostShortcutBinding? match(KeyEvent event) {
for (final HostShortcutBinding binding in bindings) {
if (binding.matches(event)) {
return binding;
}
}
return null;
}
/// Default host shortcuts used by [GameScreenInputManager].
///
/// Alt+Enter toggles fullscreen and suppresses the engine `interact` action
/// for one frame so Enter does not also activate menu/game interactions.
static const HostShortcutRegistry defaults = HostShortcutRegistry(
bindings: <HostShortcutBinding>[
HostShortcutBinding(
matches: isAltEnterShortcut,
intent: HostShortcutIntent.toggleFullscreen,
suppressedActions: <WolfInputAction>{WolfInputAction.interact},
),
],
);
}
/// Optional imperative host shortcut override.
///
/// Return true when the event was fully handled. The handler receives the
/// shared [Wolf3dFlutterInput] so it can suppress engine actions as needed.
typedef HostShortcutHandler =
bool Function(
KeyEvent event,
Wolf3dFlutterInput input,
);
/// Coordinates keyboard shortcut processing for gameplay renderer hosts.
class GameScreenInputManager {
/// Creates an input/shortcut manager for [input].
const GameScreenInputManager({
required this.input,
this.hostShortcutHandler,
this.hostShortcutRegistry = HostShortcutRegistry.defaults,
required this.onToggleFullscreen,
});
/// Shared engine input adapter.
final Wolf3dFlutterInput input;
/// Optional imperative host shortcut override.
final HostShortcutHandler? hostShortcutHandler;
/// Declarative fallback host shortcut registry.
final HostShortcutRegistry hostShortcutRegistry;
/// Callback invoked when fullscreen intent is triggered.
final Future<void> Function() onToggleFullscreen;
/// Handles key-down shortcut and renderer actions for [engine].
void handleRendererKeyEvent({
required KeyEvent event,
required WolfEngine engine,
required bool isAsciiRenderer,
required bool isHardwareRenderer,
required void Function() onFpsToggled,
}) {
if (event is! KeyDownEvent) {
return;
}
// Host shortcuts must be processed before game actions so they can
// suppress overlapping keys (for example Alt+Enter consuming Enter).
if (_handleHostShortcut(event)) {
return;
}
if (event.logicalKey == input.rendererToggleKey) {
engine.cycleRendererMode();
return;
}
if (event.logicalKey == input.fpsToggleKey) {
onFpsToggled();
return;
}
if (event.logicalKey == input.asciiThemeCycleKey) {
if (isAsciiRenderer) {
engine.cycleAsciiTheme();
} else if (isHardwareRenderer) {
engine.toggleHardwareEffects();
}
}
}
/// Handles key-down events for a host screen with nullable [engine].
///
/// This owns host-side renderer mode branching so host widgets can wire key
/// events directly without private forwarding methods.
void handleRendererKeyEventForHost({
required KeyEvent event,
required WolfEngine? engine,
required GameRendererMode rendererMode,
required void Function(WolfEngine engine) onFpsToggled,
}) {
final WolfEngine? activeEngine = engine;
if (activeEngine == null) {
return;
}
handleRendererKeyEvent(
event: event,
engine: activeEngine,
isAsciiRenderer: rendererMode == GameRendererMode.ascii,
isHardwareRenderer: rendererMode == GameRendererMode.hardware,
onFpsToggled: () => onFpsToggled(activeEngine),
);
}
bool _handleHostShortcut(KeyEvent event) {
final HostShortcutHandler? customHandler = hostShortcutHandler;
if (customHandler != null) {
// Custom handlers take full precedence to support future menu-driven
// rebinding/override systems without modifying this screen.
return customHandler(event, input);
}
final HostShortcutBinding? binding = hostShortcutRegistry.match(event);
if (binding == null) {
return false;
}
// Suppress conflicting gameplay/menu actions for one update frame.
for (final WolfInputAction action in binding.suppressedActions) {
input.suppressActionOnce(action);
}
switch (binding.intent) {
case HostShortcutIntent.toggleFullscreen:
unawaited(onToggleFullscreen());
}
return true;
}
}
/// Returns true when [event] is Enter/NumpadEnter while Alt is pressed.
bool isAltEnterShortcut(KeyEvent event) {
final bool isEnter =
event.logicalKey == LogicalKeyboardKey.enter ||
event.logicalKey == LogicalKeyboardKey.numpadEnter;
if (!isEnter) {
return false;
}
final Set<LogicalKeyboardKey> pressedKeys =
HardwareKeyboard.instance.logicalKeysPressed;
return pressedKeys.contains(LogicalKeyboardKey.altLeft) ||
pressedKeys.contains(LogicalKeyboardKey.altRight) ||
pressedKeys.contains(LogicalKeyboardKey.alt);
}