feat: Implement Change View and Renderer Options menus

- Added functionality to display and navigate the Change View menu in SixelRenderer and SoftwareRenderer.
- Introduced methods to draw the Change View and Renderer Options menus, including handling cursor and selection states.
- Updated WolfClassicMenuArt to include a customize label for the new menu.
- Enhanced WolfMenuScreen to support new menu states.
- Created tests for Change View menu interactions, ensuring proper transitions and renderer settings toggling.
- Implemented persistence for renderer settings in Flutter, allowing settings to be saved and loaded from a local file.

Signed-off-by: Hans Kokx <hans.d.kokx@gmail.com>
This commit is contained in:
2026-03-20 20:49:37 +01:00
parent 45e5302eac
commit 3270338f44
20 changed files with 2223 additions and 140 deletions

View File

@@ -0,0 +1,62 @@
/// Flutter host adapter for persisting renderer settings to a local file.
///
/// Uses `dart:io` for desktop targets where a writable path is available.
/// On web and other platforms that lack dart:io, persistence is silently
/// skipped so the rest of the app continues to work normally.
library;
import 'dart:io';
import 'package:flutter/foundation.dart';
import 'package:wolf_3d_dart/wolf_3d_engine.dart';
/// Persists [WolfRendererSettings] as JSON to the app's support directory.
///
/// This implementation relies on `dart:io` and is therefore only active on
/// non-web platforms. Pass an explicit [filePath] during testing.
class FlutterRendererSettingsPersistence extends RendererSettingsPersistence
with JsonRendererSettingsPersistence {
FlutterRendererSettingsPersistence({String? filePath}) : _filePath = filePath;
final String? _filePath;
String? _resolvedPath;
Future<String> _getFilePath() async {
if (_resolvedPath != null) {
return _resolvedPath!;
}
if (_filePath != null) {
_resolvedPath = _filePath;
return _resolvedPath!;
}
// Resolve platform app-support directory.
final String home =
Platform.environment['HOME'] ?? Platform.environment['APPDATA'] ?? '.';
_resolvedPath = '$home/.wolf3d_settings.json';
return _resolvedPath!;
}
@override
Future<String?> readRaw() async {
if (kIsWeb) return null;
try {
final String path = await _getFilePath();
final File f = File(path);
if (!f.existsSync()) return null;
return await f.readAsString();
} catch (_) {
return null;
}
}
@override
Future<void> writeRaw(String json) async {
if (kIsWeb) return;
try {
final String path = await _getFilePath();
await File(path).writeAsString(json, flush: true);
} catch (_) {
// Best-effort.
}
}
}

View File

@@ -85,6 +85,9 @@ class Wolf3d {
WolfEngine launchEngine({
required void Function() onGameWon,
void Function()? onQuit,
WolfRendererCapabilities? rendererCapabilities,
WolfRendererSettings? rendererSettings,
void Function(WolfRendererSettings settings)? onRendererSettingsChanged,
}) {
if (availableGames.isEmpty) {
throw StateError(
@@ -106,6 +109,9 @@ class Wolf3d {
// so backing out of the top-level menu should not pop the route.
onMenuExit: () {},
onQuit: onQuit,
rendererCapabilities: rendererCapabilities,
rendererSettings: rendererSettings,
onRendererSettingsChanged: onRendererSettingsChanged,
onGameSelected: (game) {
_activeGame = game;
audio.activeGame = game;