diff --git a/apps/wolf_3d_gui/lib/main.dart b/apps/wolf_3d_gui/lib/main.dart index 2b39f3d..7a42dd3 100644 --- a/apps/wolf_3d_gui/lib/main.dart +++ b/apps/wolf_3d_gui/lib/main.dart @@ -4,7 +4,9 @@ /// before presenting the game-selection flow. library; +import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; +import 'package:window_manager/window_manager.dart'; import 'package:wolf_3d_flutter/wolf_3d_flutter.dart'; import 'package:wolf_3d_gui/screens/game_screen.dart'; @@ -12,6 +14,10 @@ import 'package:wolf_3d_gui/screens/game_screen.dart'; void main() async { WidgetsFlutterBinding.ensureInitialized(); + if (_supportsDesktopWindowing) { + await windowManager.ensureInitialized(); + } + final Wolf3d wolf3d = await Wolf3d().init(); runApp( @@ -23,6 +29,19 @@ void main() async { ); } +bool get _supportsDesktopWindowing { + if (kIsWeb) { + return false; + } + + return switch (defaultTargetPlatform) { + TargetPlatform.linux || + TargetPlatform.windows || + TargetPlatform.macOS => true, + _ => false, + }; +} + class _NoGameDataScreen extends StatelessWidget { const _NoGameDataScreen(); diff --git a/apps/wolf_3d_gui/lib/screens/game_screen.dart b/apps/wolf_3d_gui/lib/screens/game_screen.dart index 14adf53..ff1b928 100644 --- a/apps/wolf_3d_gui/lib/screens/game_screen.dart +++ b/apps/wolf_3d_gui/lib/screens/game_screen.dart @@ -1,11 +1,16 @@ /// Active gameplay screen for the Flutter host. library; +import 'dart:async'; + +import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; +import 'package:window_manager/window_manager.dart'; import 'package:wolf_3d_dart/wolf_3d_engine.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_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'; @@ -16,14 +21,28 @@ enum RendererMode { hardware, } +typedef HostShortcutHandler = + bool Function( + KeyEvent event, + Wolf3dFlutterInput input, + ); + /// 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; + /// Creates a gameplay screen driven by [wolf3d]. const GameScreen({ required this.wolf3d, + this.hostShortcutHandler, super.key, }); @@ -155,6 +174,10 @@ class _GameScreenState extends State { return; } + if (_handleHostShortcut(event)) { + return; + } + if (event.logicalKey == widget.wolf3d.input.rendererToggleKey) { setState(_cycleRendererMode); return; @@ -218,4 +241,61 @@ class _GameScreenState extends State { void _toggleGlslEffects() { _glslEffectsEnabled = !_glslEffectsEnabled; } + + bool _handleHostShortcut(KeyEvent event) { + final HostShortcutHandler? customHandler = widget.hostShortcutHandler; + if (customHandler != null) { + return customHandler(event, widget.wolf3d.input); + } + + if (_isAltEnter(event)) { + // Consume Enter so fullscreen toggling does not also activate menu items. + widget.wolf3d.input.suppressInteractOnce(); + unawaited(_toggleFullscreen()); + return true; + } + + return false; + } + + bool _isAltEnter(KeyEvent event) { + final bool isEnter = + event.logicalKey == LogicalKeyboardKey.enter || + event.logicalKey == LogicalKeyboardKey.numpadEnter; + if (!isEnter) { + return false; + } + + final Set pressedKeys = + HardwareKeyboard.instance.logicalKeysPressed; + return pressedKeys.contains(LogicalKeyboardKey.altLeft) || + pressedKeys.contains(LogicalKeyboardKey.altRight) || + pressedKeys.contains(LogicalKeyboardKey.alt); + } + + Future _toggleFullscreen() async { + if (!_supportsDesktopWindowing) { + return; + } + + try { + final bool isFullScreen = await windowManager.isFullScreen(); + await windowManager.setFullScreen(!isFullScreen); + } on MissingPluginException { + // No-op on hosts where the window manager plugin is unavailable. + } + } + + bool get _supportsDesktopWindowing { + if (kIsWeb) { + return false; + } + + return switch (defaultTargetPlatform) { + TargetPlatform.linux || + TargetPlatform.windows || + TargetPlatform.macOS => true, + _ => false, + }; + } } 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 1830e5c..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,9 +7,17 @@ #include "generated_plugin_registrant.h" #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) 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) 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 e9abb91..de49b37 100644 --- a/apps/wolf_3d_gui/linux/flutter/generated_plugins.cmake +++ b/apps/wolf_3d_gui/linux/flutter/generated_plugins.cmake @@ -4,6 +4,8 @@ list(APPEND FLUTTER_PLUGIN_LIST audioplayers_linux + screen_retriever_linux + window_manager ) list(APPEND FLUTTER_FFI_PLUGIN_LIST diff --git a/apps/wolf_3d_gui/pubspec.yaml b/apps/wolf_3d_gui/pubspec.yaml index 017a964..4e47cdf 100644 --- a/apps/wolf_3d_gui/pubspec.yaml +++ b/apps/wolf_3d_gui/pubspec.yaml @@ -12,6 +12,7 @@ dependencies: wolf_3d_dart: wolf_3d_renderer: any wolf_3d_flutter: any + window_manager: ^0.5.1 flutter: sdk: flutter diff --git a/packages/wolf_3d_flutter/lib/wolf_3d_input_flutter.dart b/packages/wolf_3d_flutter/lib/wolf_3d_input_flutter.dart index ae8e20c..ebfd121 100644 --- a/packages/wolf_3d_flutter/lib/wolf_3d_input_flutter.dart +++ b/packages/wolf_3d_flutter/lib/wolf_3d_input_flutter.dart @@ -95,6 +95,7 @@ class Wolf3dFlutterInput extends Wolf3dInput { double _mouseDeltaY = 0.0; bool _previousMouseRightDown = false; bool _queuedBack = false; + final Set _suppressedActionsOnce = {}; // Mouse-look is optional so touch or keyboard-only hosts can keep the same // adapter without incurring accidental pointer-driven movement. @@ -153,8 +154,21 @@ class Wolf3dFlutterInput extends Wolf3dInput { _queuedBack = true; } + /// Suppresses [action] for the next [update] tick only. + void suppressActionOnce(WolfInputAction action) { + _suppressedActionsOnce.add(action); + } + + /// Convenience helper for host shortcuts that consume Enter/Interact. + void suppressInteractOnce() { + suppressActionOnce(WolfInputAction.interact); + } + /// Returns whether any bound key for [action] is currently pressed. bool _isActive(WolfInputAction action, Set pressedKeys) { + if (_suppressedActionsOnce.contains(action)) { + return false; + } return bindings[action]!.any((key) => pressedKeys.contains(key)); } @@ -163,6 +177,9 @@ class Wolf3dFlutterInput extends Wolf3dInput { WolfInputAction action, Set newlyPressed, ) { + if (_suppressedActionsOnce.contains(action)) { + return false; + } return bindings[action]!.any((key) => newlyPressed.contains(key)); } @@ -223,5 +240,6 @@ class Wolf3dFlutterInput extends Wolf3dInput { _previousKeys = Set.from(pressedKeys); _previousMouseRightDown = isMouseRightDown; _queuedBack = false; + _suppressedActionsOnce.clear(); } }