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:
@@ -17,6 +17,8 @@ class CliGameLoop {
|
||||
CliGameLoop({
|
||||
required this.engine,
|
||||
required this.onExit,
|
||||
this.persistence,
|
||||
this.initialSettings,
|
||||
}) : input = engine.input is CliInput
|
||||
? engine.input as CliInput
|
||||
: throw ArgumentError.value(
|
||||
@@ -37,6 +39,8 @@ class CliGameLoop {
|
||||
final CliRendererBackend secondaryRenderer;
|
||||
final CliInput input;
|
||||
final void Function(int code) onExit;
|
||||
final RendererSettingsPersistence? persistence;
|
||||
final WolfRendererSettings? initialSettings;
|
||||
|
||||
final Stopwatch _stopwatch = Stopwatch();
|
||||
final Stream<List<int>> _stdinStream = stdin.asBroadcastStream();
|
||||
@@ -59,12 +63,32 @@ class CliGameLoop {
|
||||
inputStream: _stdinStream,
|
||||
);
|
||||
_isSixelAvailable = sixel.isSixelSupported;
|
||||
_renderer = _isSixelAvailable ? primaryRenderer : secondaryRenderer;
|
||||
} else {
|
||||
_isSixelAvailable = false;
|
||||
_renderer = secondaryRenderer;
|
||||
}
|
||||
|
||||
final Set<WolfRendererMode> supportedModes = <WolfRendererMode>{
|
||||
WolfRendererMode.ascii,
|
||||
if (_isSixelAvailable) WolfRendererMode.sixel,
|
||||
};
|
||||
engine.setRendererCapabilities(
|
||||
WolfRendererCapabilities(
|
||||
supportedModes: supportedModes,
|
||||
supportsAsciiThemes: true,
|
||||
supportsFpsCounter: true,
|
||||
),
|
||||
);
|
||||
|
||||
if (initialSettings != null) {
|
||||
engine.updateRendererSettings(initialSettings!);
|
||||
} else if (_isSixelAvailable) {
|
||||
engine.updateRendererSettings(
|
||||
engine.rendererSettings.copyWith(mode: WolfRendererMode.sixel),
|
||||
);
|
||||
}
|
||||
|
||||
_syncRendererFromEngine();
|
||||
|
||||
if (stdin.hasTerminal) {
|
||||
try {
|
||||
stdin.echoMode = false;
|
||||
@@ -124,43 +148,48 @@ class CliGameLoop {
|
||||
}
|
||||
|
||||
if (input.matchesRendererToggleShortcut(bytes)) {
|
||||
if (!_isSixelAvailable) {
|
||||
return;
|
||||
}
|
||||
// Allow dynamic renderer-switch bindings configured on the CLI input.
|
||||
_renderer = identical(_renderer, secondaryRenderer)
|
||||
? primaryRenderer
|
||||
: secondaryRenderer;
|
||||
engine.cycleRendererMode();
|
||||
_syncRendererFromEngine();
|
||||
unawaited(persistence?.save(engine.rendererSettings));
|
||||
stdout.write('\x1b[2J\x1b[H');
|
||||
return;
|
||||
}
|
||||
|
||||
if (input.matchesAsciiThemeCycleShortcut(bytes)) {
|
||||
_cycleAsciiTheme();
|
||||
engine.cycleAsciiTheme();
|
||||
_syncRendererFromEngine();
|
||||
unawaited(persistence?.save(engine.rendererSettings));
|
||||
return;
|
||||
}
|
||||
|
||||
input.handleKey(bytes);
|
||||
}
|
||||
|
||||
void _cycleAsciiTheme() {
|
||||
final List<AsciiRenderer> asciiRenderers = <AsciiRenderer>[
|
||||
if (primaryRenderer is AsciiRenderer) primaryRenderer as AsciiRenderer,
|
||||
if (secondaryRenderer is AsciiRenderer)
|
||||
secondaryRenderer as AsciiRenderer,
|
||||
];
|
||||
if (asciiRenderers.isEmpty) {
|
||||
return;
|
||||
void _syncRendererFromEngine() {
|
||||
final CliRendererBackend previousRenderer = _renderer;
|
||||
final WolfRendererMode mode = engine.rendererSettings.mode;
|
||||
if (mode == WolfRendererMode.sixel && _isSixelAvailable) {
|
||||
_renderer = primaryRenderer;
|
||||
} else {
|
||||
_renderer = secondaryRenderer;
|
||||
}
|
||||
|
||||
final AsciiTheme nextTheme = AsciiThemes.nextOf(
|
||||
asciiRenderers.first.activeTheme,
|
||||
);
|
||||
for (final renderer in asciiRenderers) {
|
||||
renderer.activeTheme = nextTheme;
|
||||
final AsciiTheme theme =
|
||||
engine.rendererSettings.asciiThemeId ==
|
||||
WolfRendererSettings.asciiThemeQuadrant
|
||||
? AsciiThemes.quadrant
|
||||
: AsciiThemes.blocks;
|
||||
if (primaryRenderer is AsciiRenderer) {
|
||||
(primaryRenderer as AsciiRenderer).activeTheme = theme;
|
||||
}
|
||||
if (secondaryRenderer is AsciiRenderer) {
|
||||
(secondaryRenderer as AsciiRenderer).activeTheme = theme;
|
||||
}
|
||||
|
||||
if (stdout.hasTerminal) {
|
||||
engine.showFpsCounter = engine.rendererSettings.fpsCounterEnabled;
|
||||
|
||||
if (!identical(previousRenderer, _renderer) && stdout.hasTerminal) {
|
||||
// Clear stale frame content when switching backend output modes.
|
||||
stdout.write('\x1b[2J\x1b[H');
|
||||
}
|
||||
}
|
||||
@@ -170,6 +199,9 @@ class CliGameLoop {
|
||||
return;
|
||||
}
|
||||
|
||||
// Apply renderer changes made via in-game menus or settings updates.
|
||||
_syncRendererFromEngine();
|
||||
|
||||
if (stdout.hasTerminal) {
|
||||
final int cols = stdout.terminalColumns;
|
||||
final int rows = stdout.terminalLines;
|
||||
@@ -198,51 +230,5 @@ class CliGameLoop {
|
||||
|
||||
engine.tick(elapsed);
|
||||
stdout.write(_renderer.render(engine));
|
||||
_writeShortcutHintLine();
|
||||
}
|
||||
|
||||
void _writeShortcutHintLine() {
|
||||
if (!stdout.hasTerminal) {
|
||||
return;
|
||||
}
|
||||
|
||||
final int cols = stdout.terminalColumns;
|
||||
if (cols <= 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
final int safeCols = cols > 1 ? cols - 1 : cols;
|
||||
final String hint = _buildShortcutHintText();
|
||||
if (hint.isEmpty) {
|
||||
return;
|
||||
}
|
||||
final String visible = hint.length > safeCols
|
||||
? hint.substring(0, safeCols)
|
||||
: hint;
|
||||
final String padded = visible.padRight(safeCols);
|
||||
|
||||
// Draw an overlay line without disturbing the renderer's cursor position.
|
||||
stdout.write('\x1b[s\x1b[1;1H\x1b[0m\x1b[2m\x1b[2K$padded\x1b[0m\x1b[u');
|
||||
}
|
||||
|
||||
String _buildShortcutHintText() {
|
||||
if (!_isSixelAvailable) {
|
||||
if (_renderer is AsciiRenderer) {
|
||||
final AsciiRenderer ascii = _renderer as AsciiRenderer;
|
||||
return '<${input.asciiThemeCycleKeyLabel}> ${ascii.activeTheme.name}';
|
||||
}
|
||||
return '';
|
||||
}
|
||||
|
||||
final String rendererMode = _renderer is SixelRenderer ? 'sixel' : 'ascii';
|
||||
final String rendererHint =
|
||||
'<${input.rendererToggleKeyLabel}> $rendererMode';
|
||||
|
||||
if (_renderer is AsciiRenderer) {
|
||||
final AsciiRenderer ascii = _renderer as AsciiRenderer;
|
||||
return '$rendererHint <${input.asciiThemeCycleKeyLabel}> ${ascii.activeTheme.name}';
|
||||
}
|
||||
|
||||
return rendererHint;
|
||||
}
|
||||
}
|
||||
|
||||
42
apps/wolf_3d_cli/lib/cli_renderer_settings_persistence.dart
Normal file
42
apps/wolf_3d_cli/lib/cli_renderer_settings_persistence.dart
Normal file
@@ -0,0 +1,42 @@
|
||||
/// CLI host adapter for persisting renderer settings to a local file.
|
||||
library;
|
||||
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:wolf_3d_dart/wolf_3d_engine.dart';
|
||||
|
||||
/// Persists [WolfRendererSettings] as JSON to a local file.
|
||||
///
|
||||
/// The default path is `~/.wolf3d_cli_settings.json`.
|
||||
/// An alternative [filePath] can be supplied at construction time.
|
||||
class CliRendererSettingsPersistence extends RendererSettingsPersistence
|
||||
with JsonRendererSettingsPersistence {
|
||||
CliRendererSettingsPersistence({String? filePath})
|
||||
: _filePath =
|
||||
filePath ??
|
||||
'${Platform.environment['HOME'] ?? '.'}/.wolf3d_cli_settings.json';
|
||||
|
||||
final String _filePath;
|
||||
|
||||
@override
|
||||
Future<String?> readRaw() async {
|
||||
try {
|
||||
final File f = File(_filePath);
|
||||
if (!f.existsSync()) {
|
||||
return null;
|
||||
}
|
||||
return await f.readAsString();
|
||||
} catch (_) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> writeRaw(String json) async {
|
||||
try {
|
||||
await File(_filePath).writeAsString(json, flush: true);
|
||||
} catch (_) {
|
||||
// Best-effort; never crash the loop on a write failure.
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user