feat: Integrate window manager for desktop windowing support and enhance input handling

Signed-off-by: Hans Kokx <hans.d.kokx@gmail.com>
This commit is contained in:
2026-03-20 11:25:05 +01:00
parent 10eaef9690
commit c81eb6750d
6 changed files with 128 additions and 0 deletions

View File

@@ -4,7 +4,9 @@
/// before presenting the game-selection flow. /// before presenting the game-selection flow.
library; library;
import 'package:flutter/foundation.dart';
import 'package:flutter/material.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_flutter/wolf_3d_flutter.dart';
import 'package:wolf_3d_gui/screens/game_screen.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 { void main() async {
WidgetsFlutterBinding.ensureInitialized(); WidgetsFlutterBinding.ensureInitialized();
if (_supportsDesktopWindowing) {
await windowManager.ensureInitialized();
}
final Wolf3d wolf3d = await Wolf3d().init(); final Wolf3d wolf3d = await Wolf3d().init();
runApp( 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 { class _NoGameDataScreen extends StatelessWidget {
const _NoGameDataScreen(); const _NoGameDataScreen();

View File

@@ -1,11 +1,16 @@
/// Active gameplay screen for the Flutter host. /// Active gameplay screen for the Flutter host.
library; library;
import 'dart:async';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/services.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_engine.dart';
import 'package:wolf_3d_dart/wolf_3d_renderer.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_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_ascii_renderer.dart';
import 'package:wolf_3d_renderer/wolf_3d_flutter_renderer.dart'; import 'package:wolf_3d_renderer/wolf_3d_flutter_renderer.dart';
import 'package:wolf_3d_renderer/wolf_3d_glsl_renderer.dart'; import 'package:wolf_3d_renderer/wolf_3d_glsl_renderer.dart';
@@ -16,14 +21,28 @@ enum RendererMode {
hardware, hardware,
} }
typedef HostShortcutHandler =
bool Function(
KeyEvent event,
Wolf3dFlutterInput input,
);
/// Launches a [WolfEngine] via [Wolf3d] and exposes renderer/input integrations. /// Launches a [WolfEngine] via [Wolf3d] and exposes renderer/input integrations.
class GameScreen extends StatefulWidget { class GameScreen extends StatefulWidget {
/// Shared application facade owning the engine, audio, and input. /// Shared application facade owning the engine, audio, and input.
final Wolf3d wolf3d; 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]. /// Creates a gameplay screen driven by [wolf3d].
const GameScreen({ const GameScreen({
required this.wolf3d, required this.wolf3d,
this.hostShortcutHandler,
super.key, super.key,
}); });
@@ -155,6 +174,10 @@ class _GameScreenState extends State<GameScreen> {
return; return;
} }
if (_handleHostShortcut(event)) {
return;
}
if (event.logicalKey == widget.wolf3d.input.rendererToggleKey) { if (event.logicalKey == widget.wolf3d.input.rendererToggleKey) {
setState(_cycleRendererMode); setState(_cycleRendererMode);
return; return;
@@ -218,4 +241,61 @@ class _GameScreenState extends State<GameScreen> {
void _toggleGlslEffects() { void _toggleGlslEffects() {
_glslEffectsEnabled = !_glslEffectsEnabled; _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<LogicalKeyboardKey> pressedKeys =
HardwareKeyboard.instance.logicalKeysPressed;
return pressedKeys.contains(LogicalKeyboardKey.altLeft) ||
pressedKeys.contains(LogicalKeyboardKey.altRight) ||
pressedKeys.contains(LogicalKeyboardKey.alt);
}
Future<void> _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,
};
}
} }

View File

@@ -7,9 +7,17 @@
#include "generated_plugin_registrant.h" #include "generated_plugin_registrant.h"
#include <audioplayers_linux/audioplayers_linux_plugin.h> #include <audioplayers_linux/audioplayers_linux_plugin.h>
#include <screen_retriever_linux/screen_retriever_linux_plugin.h>
#include <window_manager/window_manager_plugin.h>
void fl_register_plugins(FlPluginRegistry* registry) { void fl_register_plugins(FlPluginRegistry* registry) {
g_autoptr(FlPluginRegistrar) audioplayers_linux_registrar = g_autoptr(FlPluginRegistrar) audioplayers_linux_registrar =
fl_plugin_registry_get_registrar_for_plugin(registry, "AudioplayersLinuxPlugin"); fl_plugin_registry_get_registrar_for_plugin(registry, "AudioplayersLinuxPlugin");
audioplayers_linux_plugin_register_with_registrar(audioplayers_linux_registrar); 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);
} }

View File

@@ -4,6 +4,8 @@
list(APPEND FLUTTER_PLUGIN_LIST list(APPEND FLUTTER_PLUGIN_LIST
audioplayers_linux audioplayers_linux
screen_retriever_linux
window_manager
) )
list(APPEND FLUTTER_FFI_PLUGIN_LIST list(APPEND FLUTTER_FFI_PLUGIN_LIST

View File

@@ -12,6 +12,7 @@ dependencies:
wolf_3d_dart: wolf_3d_dart:
wolf_3d_renderer: any wolf_3d_renderer: any
wolf_3d_flutter: any wolf_3d_flutter: any
window_manager: ^0.5.1
flutter: flutter:
sdk: flutter sdk: flutter

View File

@@ -95,6 +95,7 @@ class Wolf3dFlutterInput extends Wolf3dInput {
double _mouseDeltaY = 0.0; double _mouseDeltaY = 0.0;
bool _previousMouseRightDown = false; bool _previousMouseRightDown = false;
bool _queuedBack = false; bool _queuedBack = false;
final Set<WolfInputAction> _suppressedActionsOnce = <WolfInputAction>{};
// Mouse-look is optional so touch or keyboard-only hosts can keep the same // Mouse-look is optional so touch or keyboard-only hosts can keep the same
// adapter without incurring accidental pointer-driven movement. // adapter without incurring accidental pointer-driven movement.
@@ -153,8 +154,21 @@ class Wolf3dFlutterInput extends Wolf3dInput {
_queuedBack = true; _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. /// Returns whether any bound key for [action] is currently pressed.
bool _isActive(WolfInputAction action, Set<LogicalKeyboardKey> pressedKeys) { bool _isActive(WolfInputAction action, Set<LogicalKeyboardKey> pressedKeys) {
if (_suppressedActionsOnce.contains(action)) {
return false;
}
return bindings[action]!.any((key) => pressedKeys.contains(key)); return bindings[action]!.any((key) => pressedKeys.contains(key));
} }
@@ -163,6 +177,9 @@ class Wolf3dFlutterInput extends Wolf3dInput {
WolfInputAction action, WolfInputAction action,
Set<LogicalKeyboardKey> newlyPressed, Set<LogicalKeyboardKey> newlyPressed,
) { ) {
if (_suppressedActionsOnce.contains(action)) {
return false;
}
return bindings[action]!.any((key) => newlyPressed.contains(key)); return bindings[action]!.any((key) => newlyPressed.contains(key));
} }
@@ -223,5 +240,6 @@ class Wolf3dFlutterInput extends Wolf3dInput {
_previousKeys = Set.from(pressedKeys); _previousKeys = Set.from(pressedKeys);
_previousMouseRightDown = isMouseRightDown; _previousMouseRightDown = isMouseRightDown;
_queuedBack = false; _queuedBack = false;
_suppressedActionsOnce.clear();
} }
} }