Refactor SixelRasterizer and SoftwareRasterizer for improved terminal handling and menu rendering
Signed-off-by: Hans Kokx <hans.d.kokx@gmail.com>
This commit is contained in:
@@ -25,6 +25,7 @@ class SixelRasterizer extends CliRasterizer<String> {
|
|||||||
static const double _defaultCellWidthToHeight = 0.55;
|
static const double _defaultCellWidthToHeight = 0.55;
|
||||||
static const int _minimumTerminalColumns = 117;
|
static const int _minimumTerminalColumns = 117;
|
||||||
static const int _minimumTerminalRows = 34;
|
static const int _minimumTerminalRows = 34;
|
||||||
|
static const double _terminalViewportSafety = 0.90;
|
||||||
static const int _compactMenuMinWidthPx = 200;
|
static const int _compactMenuMinWidthPx = 200;
|
||||||
static const int _compactMenuMinHeightPx = 130;
|
static const int _compactMenuMinHeightPx = 130;
|
||||||
static const int _maxRenderWidth = 320;
|
static const int _maxRenderWidth = 320;
|
||||||
@@ -42,6 +43,16 @@ class SixelRasterizer extends CliRasterizer<String> {
|
|||||||
/// This should be updated by calling [checkTerminalSixelSupport] before the game loop.
|
/// This should be updated by calling [checkTerminalSixelSupport] before the game loop.
|
||||||
bool isSixelSupported = true;
|
bool isSixelSupported = true;
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool isTerminalSizeSupported(int columns, int rows) {
|
||||||
|
return columns >= _minimumTerminalColumns && rows >= _minimumTerminalRows;
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get terminalSizeRequirement =>
|
||||||
|
'Sixel renderer requires a minimum resolution of '
|
||||||
|
'${_minimumTerminalColumns}x$_minimumTerminalRows.';
|
||||||
|
|
||||||
// ===========================================================================
|
// ===========================================================================
|
||||||
// TERMINAL AUTODETECT
|
// TERMINAL AUTODETECT
|
||||||
// ===========================================================================
|
// ===========================================================================
|
||||||
@@ -144,46 +155,54 @@ class SixelRasterizer extends CliRasterizer<String> {
|
|||||||
final int previousOutputWidth = _outputWidth;
|
final int previousOutputWidth = _outputWidth;
|
||||||
final int previousOutputHeight = _outputHeight;
|
final int previousOutputHeight = _outputHeight;
|
||||||
|
|
||||||
// First fit a terminal cell rectangle that respects the minimum usable
|
// Fit the sixel output inside the full terminal viewport while preserving
|
||||||
// column/row envelope for status text and centered output.
|
// a 4:3 presentation.
|
||||||
final double fitScale = math.min(
|
final int terminalColumns = math.max(1, terminalBuffer.width);
|
||||||
terminalBuffer.width / _minimumTerminalColumns,
|
final int terminalRows = math.max(1, terminalBuffer.height);
|
||||||
terminalBuffer.height / _minimumTerminalRows,
|
final double cellPixelWidth =
|
||||||
);
|
_defaultLineHeightPx * _defaultCellWidthToHeight;
|
||||||
|
|
||||||
final int targetColumns = math.max(
|
|
||||||
1,
|
|
||||||
(_minimumTerminalColumns * fitScale).floor(),
|
|
||||||
);
|
|
||||||
final int targetRows = math.max(
|
|
||||||
1,
|
|
||||||
(_minimumTerminalRows * fitScale).floor(),
|
|
||||||
);
|
|
||||||
|
|
||||||
_offsetColumns = math.max(0, (terminalBuffer.width - targetColumns) ~/ 2);
|
|
||||||
_offsetRows = math.max(0, (terminalBuffer.height - targetRows) ~/ 2);
|
|
||||||
|
|
||||||
final int boundsPixelWidth = math.max(
|
final int boundsPixelWidth = math.max(
|
||||||
1,
|
1,
|
||||||
(targetColumns * _defaultLineHeightPx * _defaultCellWidthToHeight)
|
(terminalColumns * cellPixelWidth).floor(),
|
||||||
.floor(),
|
|
||||||
);
|
);
|
||||||
final int boundsPixelHeight = math.max(
|
final int boundsPixelHeight = math.max(
|
||||||
1,
|
1,
|
||||||
targetRows * _defaultLineHeightPx,
|
terminalRows * _defaultLineHeightPx,
|
||||||
|
);
|
||||||
|
|
||||||
|
// Terminal emulators can report approximate cell metrics, so reserve a
|
||||||
|
// safety margin to avoid right/bottom clipping and drift.
|
||||||
|
final int safePixelWidth = math.max(
|
||||||
|
1,
|
||||||
|
(boundsPixelWidth * _terminalViewportSafety).floor(),
|
||||||
|
);
|
||||||
|
final int safePixelHeight = math.max(
|
||||||
|
1,
|
||||||
|
(boundsPixelHeight * _terminalViewportSafety).floor(),
|
||||||
);
|
);
|
||||||
|
|
||||||
// Then translate terminal cells into approximate pixels so the Sixel image
|
// Then translate terminal cells into approximate pixels so the Sixel image
|
||||||
// lands on a 4:3 surface inside the available bounds.
|
// lands on a 4:3 surface inside the available bounds.
|
||||||
final double boundsAspect = boundsPixelWidth / boundsPixelHeight;
|
final double boundsAspect = safePixelWidth / safePixelHeight;
|
||||||
if (boundsAspect > _targetAspectRatio) {
|
if (boundsAspect > _targetAspectRatio) {
|
||||||
_outputHeight = boundsPixelHeight;
|
_outputHeight = safePixelHeight;
|
||||||
_outputWidth = math.max(1, (_outputHeight * _targetAspectRatio).floor());
|
_outputWidth = math.max(1, (_outputHeight * _targetAspectRatio).floor());
|
||||||
} else {
|
} else {
|
||||||
_outputWidth = boundsPixelWidth;
|
_outputWidth = safePixelWidth;
|
||||||
_outputHeight = math.max(1, (_outputWidth / _targetAspectRatio).floor());
|
_outputHeight = math.max(1, (_outputWidth / _targetAspectRatio).floor());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Horizontal: cell-width estimates vary by terminal/font and cause right-shift
|
||||||
|
// clipping, so keep the image at column 0.
|
||||||
|
// Vertical: line-height is reliable enough to center correctly.
|
||||||
|
final int imageRows = math.max(
|
||||||
|
1,
|
||||||
|
(_outputHeight / _defaultLineHeightPx).ceil(),
|
||||||
|
);
|
||||||
|
_offsetColumns = 0;
|
||||||
|
_offsetRows = math.max(0, (terminalRows - imageRows) ~/ 2);
|
||||||
|
|
||||||
if (_offsetColumns != previousOffsetColumns ||
|
if (_offsetColumns != previousOffsetColumns ||
|
||||||
_offsetRows != previousOffsetRows ||
|
_offsetRows != previousOffsetRows ||
|
||||||
_outputWidth != previousOutputWidth ||
|
_outputWidth != previousOutputWidth ||
|
||||||
@@ -355,23 +374,24 @@ class SixelRasterizer extends CliRasterizer<String> {
|
|||||||
scale: 1,
|
scale: 1,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
_drawMenuFooterArt(art);
|
||||||
_applyMenuFade(engine.menuManager.transitionAlpha, bgColor);
|
_applyMenuFade(engine.menuManager.transitionAlpha, bgColor);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (engine.menuManager.activeMenu == WolfMenuScreen.episodeSelect) {
|
if (engine.menuManager.activeMenu == WolfMenuScreen.episodeSelect) {
|
||||||
_fillRect320(12, 18, 296, 168, panelColor);
|
_fillRect320(12, 20, 296, 158, panelColor);
|
||||||
_drawMenuTextCentered(
|
_drawMenuTextCentered(
|
||||||
'WHICH EPISODE TO PLAY?',
|
'WHICH EPISODE TO PLAY?',
|
||||||
8,
|
6,
|
||||||
headingIndex,
|
headingIndex,
|
||||||
scale: 2,
|
scale: 2,
|
||||||
);
|
);
|
||||||
final cursor = art.pic(
|
final cursor = art.pic(
|
||||||
engine.menuManager.isCursorAltFrame(engine.timeAliveMs) ? 9 : 8,
|
engine.menuManager.isCursorAltFrame(engine.timeAliveMs) ? 9 : 8,
|
||||||
);
|
);
|
||||||
const int rowYStart = 24;
|
const int rowYStart = 30;
|
||||||
const int rowStep = 26;
|
const int rowStep = 24;
|
||||||
for (int i = 0; i < engine.data.episodes.length; i++) {
|
for (int i = 0; i < engine.data.episodes.length; i++) {
|
||||||
final int y = rowYStart + (i * rowStep);
|
final int y = rowYStart + (i * rowStep);
|
||||||
final bool isSelected = i == engine.menuManager.selectedEpisodeIndex;
|
final bool isSelected = i == engine.menuManager.selectedEpisodeIndex;
|
||||||
@@ -396,12 +416,13 @@ class SixelRasterizer extends CliRasterizer<String> {
|
|||||||
_drawMenuText(
|
_drawMenuText(
|
||||||
parts.sublist(1).join(' '),
|
parts.sublist(1).join(' '),
|
||||||
98,
|
98,
|
||||||
y + 13,
|
y + 12,
|
||||||
isSelected ? selectedTextIndex : unselectedTextIndex,
|
isSelected ? selectedTextIndex : unselectedTextIndex,
|
||||||
scale: 1,
|
scale: 1,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
_drawMenuFooterArt(art);
|
||||||
_applyMenuFade(engine.menuManager.transitionAlpha, bgColor);
|
_applyMenuFade(engine.menuManager.transitionAlpha, bgColor);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -456,9 +477,18 @@ class SixelRasterizer extends CliRasterizer<String> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
_drawMenuFooterArt(art);
|
||||||
_applyMenuFade(engine.menuManager.transitionAlpha, bgColor);
|
_applyMenuFade(engine.menuManager.transitionAlpha, bgColor);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void _drawMenuFooterArt(WolfClassicMenuArt art) {
|
||||||
|
final bottom = art.pic(15);
|
||||||
|
if (bottom == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
_blitVgaImage(bottom, (320 - bottom.width) ~/ 2, 200 - bottom.height - 8);
|
||||||
|
}
|
||||||
|
|
||||||
String _gameTitle(GameVersion version) {
|
String _gameTitle(GameVersion version) {
|
||||||
switch (version) {
|
switch (version) {
|
||||||
case GameVersion.shareware:
|
case GameVersion.shareware:
|
||||||
@@ -642,6 +672,7 @@ class SixelRasterizer extends CliRasterizer<String> {
|
|||||||
String toSixelString() {
|
String toSixelString() {
|
||||||
StringBuffer sb = StringBuffer();
|
StringBuffer sb = StringBuffer();
|
||||||
sb.write('\x1bPq');
|
sb.write('\x1bPq');
|
||||||
|
sb.write('"1;1;$_outputWidth;$_outputHeight');
|
||||||
|
|
||||||
double damageIntensity = engine.difficulty == null
|
double damageIntensity = engine.difficulty == null
|
||||||
? 0.0
|
? 0.0
|
||||||
|
|||||||
@@ -12,6 +12,9 @@ import 'package:wolf_3d_dart/wolf_3d_menu.dart';
|
|||||||
/// This is the canonical "modern framebuffer" implementation and serves as a
|
/// This is the canonical "modern framebuffer" implementation and serves as a
|
||||||
/// visual reference for terminal renderers.
|
/// visual reference for terminal renderers.
|
||||||
class SoftwareRasterizer extends Rasterizer<FrameBuffer> {
|
class SoftwareRasterizer extends Rasterizer<FrameBuffer> {
|
||||||
|
static const int _menuFooterY = 184;
|
||||||
|
static const int _menuFooterHeight = 12;
|
||||||
|
|
||||||
late FrameBuffer _buffer;
|
late FrameBuffer _buffer;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@@ -170,6 +173,8 @@ class SoftwareRasterizer extends Rasterizer<FrameBuffer> {
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
_drawCenteredMenuFooter(art);
|
||||||
|
|
||||||
_applyMenuFade(engine.menuManager.transitionAlpha, bgColor);
|
_applyMenuFade(engine.menuManager.transitionAlpha, bgColor);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -222,18 +227,18 @@ class SoftwareRasterizer extends Rasterizer<FrameBuffer> {
|
|||||||
int unselectedTextColor,
|
int unselectedTextColor,
|
||||||
) {
|
) {
|
||||||
const int panelX = 12;
|
const int panelX = 12;
|
||||||
const int panelY = 18;
|
const int panelY = 20;
|
||||||
const int panelW = 296;
|
const int panelW = 296;
|
||||||
const int panelH = 168;
|
const int panelH = 158;
|
||||||
_fillMenuPanel(panelX, panelY, panelW, panelH, panelColor);
|
_fillMenuPanel(panelX, panelY, panelW, panelH, panelColor);
|
||||||
|
|
||||||
_drawMenuTextCentered('WHICH EPISODE TO PLAY?', 2, headingColor, scale: 2);
|
_drawMenuTextCentered('WHICH EPISODE TO PLAY?', 6, headingColor, scale: 2);
|
||||||
|
|
||||||
final cursor = art.pic(
|
final cursor = art.pic(
|
||||||
engine.menuManager.isCursorAltFrame(engine.timeAliveMs) ? 9 : 8,
|
engine.menuManager.isCursorAltFrame(engine.timeAliveMs) ? 9 : 8,
|
||||||
);
|
);
|
||||||
const int rowYStart = 24;
|
const int rowYStart = 30;
|
||||||
const int rowStep = 26;
|
const int rowStep = 24;
|
||||||
const int imageX = 40;
|
const int imageX = 40;
|
||||||
const int textX = 98;
|
const int textX = 98;
|
||||||
|
|
||||||
@@ -264,13 +269,58 @@ class SoftwareRasterizer extends Rasterizer<FrameBuffer> {
|
|||||||
_drawMenuText(
|
_drawMenuText(
|
||||||
parts.sublist(1).join(' '),
|
parts.sublist(1).join(' '),
|
||||||
textX,
|
textX,
|
||||||
y + 13,
|
y + 12,
|
||||||
isSelected ? selectedTextColor : unselectedTextColor,
|
isSelected ? selectedTextColor : unselectedTextColor,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void _drawCenteredMenuFooter(WolfClassicMenuArt art) {
|
||||||
|
final bottom = art.pic(15);
|
||||||
|
if (bottom != null) {
|
||||||
|
final int x = ((width - bottom.width) ~/ 2).clamp(0, width - 1);
|
||||||
|
final int y = (height - bottom.height - 8).clamp(0, height - 1);
|
||||||
|
_blitVgaImage(bottom, x, y);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
final int hintKeyColor = ColorPalette.vga32Bit[12];
|
||||||
|
final int hintLabelColor = ColorPalette.vga32Bit[4];
|
||||||
|
final int hintBackground = ColorPalette.vga32Bit[0];
|
||||||
|
|
||||||
|
final List<(String text, int color)> segments = <(String, int)>[
|
||||||
|
('UP/DN', hintKeyColor),
|
||||||
|
(' MOVE ', hintLabelColor),
|
||||||
|
('RET', hintKeyColor),
|
||||||
|
(' SELECT ', hintLabelColor),
|
||||||
|
('ESC', hintKeyColor),
|
||||||
|
(' BACK', hintLabelColor),
|
||||||
|
];
|
||||||
|
|
||||||
|
int textWidth = 0;
|
||||||
|
for (final (text, _) in segments) {
|
||||||
|
textWidth += WolfMenuFont.measureTextWidth(text, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
final int panelWidth = (textWidth + 12).clamp(1, width);
|
||||||
|
final int panelX = ((width - panelWidth) ~/ 2).clamp(0, width - 1);
|
||||||
|
_fillMenuPanel(
|
||||||
|
panelX,
|
||||||
|
_menuFooterY,
|
||||||
|
panelWidth,
|
||||||
|
_menuFooterHeight,
|
||||||
|
hintBackground,
|
||||||
|
);
|
||||||
|
|
||||||
|
int cursorX = panelX + 6;
|
||||||
|
const int textY = _menuFooterY + 2;
|
||||||
|
for (final (text, color) in segments) {
|
||||||
|
_drawMenuText(text, cursorX, textY, color, scale: 1);
|
||||||
|
cursorX += WolfMenuFont.measureTextWidth(text, 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
void _drawDifficultyMenu(
|
void _drawDifficultyMenu(
|
||||||
WolfEngine engine,
|
WolfEngine engine,
|
||||||
WolfClassicMenuArt art,
|
WolfClassicMenuArt art,
|
||||||
|
|||||||
@@ -99,7 +99,9 @@ class Wolf3d {
|
|||||||
engineAudio: audio,
|
engineAudio: audio,
|
||||||
input: input,
|
input: input,
|
||||||
onGameWon: onGameWon,
|
onGameWon: onGameWon,
|
||||||
onMenuExit: onGameWon,
|
// In Flutter we keep the renderer screen active while browsing menus,
|
||||||
|
// so backing out of the top-level menu should not pop the route.
|
||||||
|
onMenuExit: () {},
|
||||||
onGameSelected: (game) {
|
onGameSelected: (game) {
|
||||||
_activeGame = game;
|
_activeGame = game;
|
||||||
audio.activeGame = game;
|
audio.activeGame = game;
|
||||||
|
|||||||
Reference in New Issue
Block a user