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:
@@ -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);
|
||||
}
|
||||
Reference in New Issue
Block a user