using System.Text;
using XFESystem.Console;
using XFESystem.Desktop;
namespace XFESystem.NativeRuntime.ConsoleApplication;
/// <summary>The earlier Rust text workspace's bitmap fonts, palette and 104/32px layout, with a standalone C# renderer.</summary>
public sealed class ClassicConsoleRenderer
{
public const uint Background = 0x09121B, Foreground = 0x75C9F5, Panel = 0x0E1C2A, Accent = 0x1CC0E8, Muted = 0x889FB4;
private readonly IConsoleFramebuffer framebuffer;
private readonly int[] pixels;
private readonly byte[] mono, unicode, icon;
public int Width => framebuffer.Width;
public int Height => framebuffer.Height;
public int ScalePercent { get; }
private int Scale(int value) => (value * ScalePercent + 50) / 100;
public int CellWidth => Scale(12);
public int CellHeight => Scale(20);
public int Margin => Scale(Width * 100L / ScalePercent >= 800 ? 24 : 16);
public int BodyTop => Scale(104);
public int Columns => Math.Max(1, (Width - 2 * Margin) / CellWidth);
public int Rows => Math.Max(1, (Height - BodyTop - Scale(44)) / CellHeight);
public long FramesPresented { get; private set; }
public ReadOnlyMemory<int> Pixels => pixels;
public ClassicConsoleRenderer(IConsoleFramebuffer framebuffer)
{
ArgumentNullException.ThrowIfNull(framebuffer);
if (framebuffer.Width is < 320 or > 16384 || framebuffer.Height is < 240 or > 16384 ||
(long)framebuffer.Width * framebuffer.Height > 33_554_432) throw new ArgumentOutOfRangeException(nameof(framebuffer));
this.framebuffer = framebuffer;
ScalePercent = Width >= 5120 || Height >= 2880 ? 300 : Width >= 3840 || Height >= 2160 ? 200 : Width >= 2560 || Height >= 1440 ? 150 : 100;
pixels = new int[checked(Width * Height)];
mono = ReadAsset("Mono.bin", 96 * 12 * 20); unicode = ReadAsset("Unicode.bin", 993201); icon = ReadAsset("Icon.rgba", 64 * 64 * 4);
}
public void Render(TerminalShell shell, bool busy = false, bool cursorVisible = true, int scrollRows = 0)
{
ArgumentNullException.ThrowIfNull(shell);
Array.Fill(pixels, (int)Background);
Chrome(shell.Version, shell.InsertMode, busy, scrollRows);
var output = shell.ReadOutput();
// The prompt is an independent live line: editing it never rewrites terminal output or reparses ANSI.
var input = new TerminalBuffer(Columns, Math.Max(2, TerminalShell.MaximumInputCharacters / Columns + 4));
input.Write(shell.Prompt + shell.Input); var inputSnapshot = input.Snapshot();
int inputRows = busy ? 0 : Math.Min(Rows, inputSnapshot.CursorRow + 1);
int available = Rows - inputRows;
int end = output.CursorRow;
bool occupied = output.CursorColumn != 0;
for (int column = 0; !occupied && column < output.Columns; column++)
{
var cell = output.GetCell(column, output.CursorRow);
occupied = !string.IsNullOrWhiteSpace(cell.Text) || cell.Style.DisplayBackground != TerminalColor.ThemeBackground;
}
if (occupied) end++;
end = Math.Clamp(end - Math.Max(0, scrollRows), 0, output.Rows);
int start = Math.Max(0, end - available), count = end - start;
for (int row = 0; row < count; row++) DrawRow(output, start + row, row);
if (!busy)
{
int inputStart = Math.Max(0, inputSnapshot.CursorRow + 1 - inputRows);
var caret = new TerminalBuffer(Columns, input.Rows); caret.Write(shell.Prompt + shell.Input[..shell.Caret]);
// Keep the current caret visible even for a long line on a small framebuffer.
inputStart = Math.Clamp(caret.CursorRow - inputRows + 1, 0, Math.Max(0, inputSnapshot.CursorRow + 1 - inputRows));
for (int row = 0; row < inputRows; row++) DrawRow(inputSnapshot, inputStart + row, count + row);
if (cursorVisible && caret.CursorRow >= inputStart && caret.CursorRow < inputStart + inputRows)
Fill(Accent, Margin + caret.CursorColumn * CellWidth, BodyTop + (count + caret.CursorRow - inputStart + 1) * CellHeight - Scale(2), CellWidth, Scale(2));
}
framebuffer.Present(pixels);
FramesPresented++;
}
private void Chrome(string version, bool insert, bool busy, int scrollRows)
{
Fill(Panel, 0, 0, Width, Scale(88)); Fill(Accent, 0, 0, Width, Scale(3));
int size = Scale(64);
for (int y = 0; y < size; y++)
for (int x = 0; x < size; x++)
{
int at = (y * 64 / size * 64 + x * 64 / size) * 4;
Pixel(Margin + x, Scale(16) + y, Blend((uint)(icon[at] << 16 | icon[at + 1] << 8 | icon[at + 2]), Panel, icon[at + 3]));
}
Label("XFESystem", Margin + Scale(80), Scale(12), Foreground, Scale(18), Scale(30));
Label("命令工作台", Margin + Scale(80), Scale(52), Muted);
if (Width >= Scale(640))
{
Label(version, Width - Margin - version.Length * CellWidth, Scale(18), Muted);
Label("zh-CN", Width - Margin - 5 * CellWidth, Scale(52), Accent);
}
Fill(0x1C364A, Margin, Scale(91), Width - Margin * 2, Scale(1));
Fill(Panel, 0, Height - Scale(32), Width, Scale(32)); Fill(0x1C364A, 0, Height - Scale(32), Width, Scale(1));
string status = busy ? "程序运行中 / Running" : scrollRows > 0 ? "输出回看 / Scrollback" : "help 查询帮助 | RAM 会话,关机不保存";
Label(status, Margin, Height - Scale(26), Muted);
if (Width >= Scale(800)) Label(insert ? "INS" : "OVR", Width - Margin - CellWidth * 3, Height - Scale(26), Accent);
}
private void DrawRow(TerminalSnapshot snapshot, int sourceRow, int targetRow)
{
if (sourceRow >= snapshot.Rows || targetRow >= Rows) return;
for (int column = 0; column < Math.Min(Columns, snapshot.Columns); column++)
{
var cell = snapshot.GetCell(column, sourceRow); if (cell.IsContinuation) continue;
uint foreground = Color(cell.Style.DisplayForeground), background = Color(cell.Style.DisplayBackground);
int x = Margin + column * CellWidth, y = BodyTop + targetRow * CellHeight;
Fill(background, x, y, CellWidth * cell.DisplayWidth, CellHeight);
foreach (var rune in cell.Text.EnumerateRunes())
Glyph(rune.Value, x, y, foreground, CellWidth, CellHeight, cell.DisplayWidth);
if (cell.Style.Underline) Fill(foreground, x, y + CellHeight - Scale(3), CellWidth * cell.DisplayWidth, Scale(1));
if (cell.Style.Strike) Fill(foreground, x, y + CellHeight / 2, CellWidth * cell.DisplayWidth, Scale(1));
}
}
private void Label(string text, int x, int y, uint color, int cellWidth = 0, int cellHeight = 0)
{
if (cellWidth == 0) cellWidth = CellWidth; if (cellHeight == 0) cellHeight = CellHeight;
foreach (var rune in text.EnumerateRunes())
{
int width = IsWide(rune.Value) ? 2 : 1;
if (x + width * cellWidth > Width - Margin) break;
Glyph(rune.Value, x, y, color, cellWidth, cellHeight, width); x += width * cellWidth;
}
}
private void Glyph(int character, int left, int top, uint color, int cellWidth, int cellHeight, int cells)
{
bool latin = character is >= 32 and <= 127;
int offset = latin ? (character - 32) * 240 : UnicodeOffset(character);
int glyphWidth = latin ? 12 : unicode[offset];
int renderWidth = Math.Min(glyphWidth, cells * 8), dx = (cells * 12 - renderWidth) / 2;
for (int y = 0; y < cellHeight; y++)
for (int x = 0; x < cells * cellWidth; x++)
{
int sx = x * 12 / cellWidth, sy = y * 20 / cellHeight;
byte alpha;
if (latin) alpha = mono[offset + sy * 12 + Math.Min(11, sx)];
else if (sy is >= 2 and < 18 && sx >= dx && sx < dx + renderWidth)
{
int row = unicode[offset + 1 + (sy - 2) * 2] << 8 | unicode[offset + 2 + (sy - 2) * 2];
bool ink = glyphWidth == 16 && renderWidth == 8 ? (row & (3 << (14 - (sx - dx) * 2))) != 0 : (row & (1 << (15 - (sx - dx)))) != 0;
alpha = ink ? (byte)255 : (byte)0;
}
else alpha = 0;
int px = left + x, py = top + y;
if (alpha != 0 && px >= 0 && py >= 0 && px < Width && py < Height)
pixels[py * Width + px] = (int)Blend(color, (uint)pixels[py * Width + px], alpha);
}
}
private static int UnicodeOffset(int character) => character switch
{
>= 128 and <= 2047 => (character - 128) * 33,
>= 8192 and <= 8303 => 63360 + (character - 8192) * 33,
>= 8592 and <= 8703 => 67056 + (character - 8592) * 33,
>= 12288 and <= 12351 => 70752 + (character - 12288) * 33,
>= 13312 and <= 40959 => 72864 + (character - 13312) * 33,
>= 65280 and <= 65519 => 985248 + (character - 65280) * 33,
_ => 993168
};
private static bool IsWide(int value) => value is >= 0x1100 and <= 0x115F or 0x2329 or 0x232A or >= 0x2E80 and <= 0xA4CF or
>= 0xAC00 and <= 0xD7A3 or >= 0xF900 and <= 0xFAFF or >= 0xFE10 and <= 0xFE19 or >= 0xFE30 and <= 0xFE6F or
>= 0xFF00 and <= 0xFF60 or >= 0xFFE0 and <= 0xFFE6 or >= 0x1F300 and <= 0x1FAFF or >= 0x20000 and <= 0x3FFFD;
private static uint Color(TerminalColor color) => color == TerminalColor.ThemeBackground ? Background : (uint)(color.Red << 16 | color.Green << 8 | color.Blue);
private static uint Blend(uint foreground, uint background, byte alpha)
{
uint result = 0;
for (int shift = 0; shift <= 16; shift += 8)
result |= ((((foreground >> shift & 255) * alpha + (background >> shift & 255) * (uint)(255 - alpha) + 127) / 255) << shift);
return result;
}
private void Pixel(int x, int y, uint color)
{ if (x >= 0 && x < Width && y >= 0 && y < Height) pixels[y * Width + x] = (int)color; }
private void Fill(uint color, int x, int y, int width, int height)
{
int right = Math.Min(Width, x + width), bottom = Math.Min(Height, y + height);
for (int row = Math.Max(0, y); row < bottom; row++)
if (right > Math.Max(0, x)) pixels.AsSpan(row * Width + Math.Max(0, x), right - Math.Max(0, x)).Fill((int)color);
}
private static byte[] ReadAsset(string name, int length)
{
using var stream = typeof(ClassicConsoleRenderer).Assembly.GetManifestResourceStream("ConsoleAssets/" + name)
?? throw new InvalidDataException("Missing classic console asset: " + name);
if (stream.Length != length) throw new InvalidDataException("Invalid classic console asset: " + name);
byte[] result = new byte[length]; stream.ReadExactly(result); return result;
}
}
using System.Text;
using XFESystem.Console;
using XFESystem.Desktop;
namespace XFESystem.NativeRuntime.ConsoleApplication;
/// <summary>The earlier Rust text workspace's bitmap fonts, palette and 104/32px layout, with a standalone C# renderer.</summary>
public sealed class ClassicConsoleRenderer
{
public const uint Background = 0x09121B, Foreground = 0x75C9F5, Panel = 0x0E1C2A, Accent = 0x1CC0E8, Muted = 0x889FB4;
private readonly IConsoleFramebuffer framebuffer;
private readonly int[] pixels;
private readonly byte[] mono, unicode, icon;
public int Width => framebuffer.Width;
public int Height => framebuffer.Height;
public int ScalePercent { get; }
private int Scale(int value) => (value * ScalePercent + 50) / 100;
public int CellWidth => Scale(12);
public int CellHeight => Scale(20);
public int Margin => Scale(Width * 100L / ScalePercent >= 800 ? 24 : 16);
public int BodyTop => Scale(104);
public int Columns => Math.Max(1, (Width - 2 * Margin) / CellWidth);
public int Rows => Math.Max(1, (Height - BodyTop - Scale(44)) / CellHeight);
public long FramesPresented { get; private set; }
public ReadOnlyMemory<int> Pixels => pixels;
public ClassicConsoleRenderer(IConsoleFramebuffer framebuffer)
{
ArgumentNullException.ThrowIfNull(framebuffer);
if (framebuffer.Width is < 320 or > 16384 || framebuffer.Height is < 240 or > 16384 ||
(long)framebuffer.Width * framebuffer.Height > 33_554_432) throw new ArgumentOutOfRangeException(nameof(framebuffer));
this.framebuffer = framebuffer;
ScalePercent = Width >= 5120 || Height >= 2880 ? 300 : Width >= 3840 || Height >= 2160 ? 200 : Width >= 2560 || Height >= 1440 ? 150 : 100;
pixels = new int[checked(Width * Height)];
mono = ReadAsset("Mono.bin", 96 * 12 * 20); unicode = ReadAsset("Unicode.bin", 993201); icon = ReadAsset("Icon.rgba", 64 * 64 * 4);
}
public void Render(TerminalShell shell, bool busy = false, bool cursorVisible = true, int scrollRows = 0)
{
ArgumentNullException.ThrowIfNull(shell);
Array.Fill(pixels, (int)Background);
Chrome(shell.Version, shell.InsertMode, busy, scrollRows);
var output = shell.ReadOutput();
// The prompt is an independent live line: editing it never rewrites terminal output or reparses ANSI.
var input = new TerminalBuffer(Columns, Math.Max(2, TerminalShell.MaximumInputCharacters / Columns + 4));
input.Write(shell.Prompt + shell.Input); var inputSnapshot = input.Snapshot();
int inputRows = busy ? 0 : Math.Min(Rows, inputSnapshot.CursorRow + 1);
int available = Rows - inputRows;
int end = output.CursorRow;
bool occupied = output.CursorColumn != 0;
for (int column = 0; !occupied && column < output.Columns; column++)
{
var cell = output.GetCell(column, output.CursorRow);
occupied = !string.IsNullOrWhiteSpace(cell.Text) || cell.Style.DisplayBackground != TerminalColor.ThemeBackground;
}
if (occupied) end++;
end = Math.Clamp(end - Math.Max(0, scrollRows), 0, output.Rows);
int start = Math.Max(0, end - available), count = end - start;
for (int row = 0; row < count; row++) DrawRow(output, start + row, row);
if (!busy)
{
int inputStart = Math.Max(0, inputSnapshot.CursorRow + 1 - inputRows);
var caret = new TerminalBuffer(Columns, input.Rows); caret.Write(shell.Prompt + shell.Input[..shell.Caret]);
// Keep the current caret visible even for a long line on a small framebuffer.
inputStart = Math.Clamp(caret.CursorRow - inputRows + 1, 0, Math.Max(0, inputSnapshot.CursorRow + 1 - inputRows));
for (int row = 0; row < inputRows; row++) DrawRow(inputSnapshot, inputStart + row, count + row);
if (cursorVisible && caret.CursorRow >= inputStart && caret.CursorRow < inputStart + inputRows)
Fill(Accent, Margin + caret.CursorColumn * CellWidth, BodyTop + (count + caret.CursorRow - inputStart + 1) * CellHeight - Scale(2), CellWidth, Scale(2));
}
framebuffer.Present(pixels);
FramesPresented++;
}
private void Chrome(string version, bool insert, bool busy, int scrollRows)
{
Fill(Panel, 0, 0, Width, Scale(88)); Fill(Accent, 0, 0, Width, Scale(3));
int size = Scale(64);
for (int y = 0; y < size; y++)
for (int x = 0; x < size; x++)
{
int at = (y * 64 / size * 64 + x * 64 / size) * 4;
Pixel(Margin + x, Scale(16) + y, Blend((uint)(icon[at] << 16 | icon[at + 1] << 8 | icon[at + 2]), Panel, icon[at + 3]));
}
Label("XFESystem", Margin + Scale(80), Scale(12), Foreground, Scale(18), Scale(30));
Label("命令工作台", Margin + Scale(80), Scale(52), Muted);
if (Width >= Scale(640))
{
Label(version, Width - Margin - version.Length * CellWidth, Scale(18), Muted);
Label("zh-CN", Width - Margin - 5 * CellWidth, Scale(52), Accent);
}
Fill(0x1C364A, Margin, Scale(91), Width - Margin * 2, Scale(1));
Fill(Panel, 0, Height - Scale(32), Width, Scale(32)); Fill(0x1C364A, 0, Height - Scale(32), Width, Scale(1));
string status = busy ? "程序运行中 / Running" : scrollRows > 0 ? "输出回看 / Scrollback" : "help 查询帮助 | RAM 会话,关机不保存";
Label(status, Margin, Height - Scale(26), Muted);
if (Width >= Scale(800)) Label(insert ? "INS" : "OVR", Width - Margin - CellWidth * 3, Height - Scale(26), Accent);
}
private void DrawRow(TerminalSnapshot snapshot, int sourceRow, int targetRow)
{
if (sourceRow >= snapshot.Rows || targetRow >= Rows) return;
for (int column = 0; column < Math.Min(Columns, snapshot.Columns); column++)
{
var cell = snapshot.GetCell(column, sourceRow); if (cell.IsContinuation) continue;
uint foreground = Color(cell.Style.DisplayForeground), background = Color(cell.Style.DisplayBackground);
int x = Margin + column * CellWidth, y = BodyTop + targetRow * CellHeight;
Fill(background, x, y, CellWidth * cell.DisplayWidth, CellHeight);
foreach (var rune in cell.Text.EnumerateRunes())
Glyph(rune.Value, x, y, foreground, CellWidth, CellHeight, cell.DisplayWidth);
if (cell.Style.Underline) Fill(foreground, x, y + CellHeight - Scale(3), CellWidth * cell.DisplayWidth, Scale(1));
if (cell.Style.Strike) Fill(foreground, x, y + CellHeight / 2, CellWidth * cell.DisplayWidth, Scale(1));
}
}
private void Label(string text, int x, int y, uint color, int cellWidth = 0, int cellHeight = 0)
{
if (cellWidth == 0) cellWidth = CellWidth; if (cellHeight == 0) cellHeight = CellHeight;
foreach (var rune in text.EnumerateRunes())
{
int width = IsWide(rune.Value) ? 2 : 1;
if (x + width * cellWidth > Width - Margin) break;
Glyph(rune.Value, x, y, color, cellWidth, cellHeight, width); x += width * cellWidth;
}
}
private void Glyph(int character, int left, int top, uint color, int cellWidth, int cellHeight, int cells)
{
bool latin = character is >= 32 and <= 127;
int offset = latin ? (character - 32) * 240 : UnicodeOffset(character);
int glyphWidth = latin ? 12 : unicode[offset];
int renderWidth = Math.Min(glyphWidth, cells * 8), dx = (cells * 12 - renderWidth) / 2;
for (int y = 0; y < cellHeight; y++)
for (int x = 0; x < cells * cellWidth; x++)
{
int sx = x * 12 / cellWidth, sy = y * 20 / cellHeight;
byte alpha;
if (latin) alpha = mono[offset + sy * 12 + Math.Min(11, sx)];
else if (sy is >= 2 and < 18 && sx >= dx && sx < dx + renderWidth)
{
int row = unicode[offset + 1 + (sy - 2) * 2] << 8 | unicode[offset + 2 + (sy - 2) * 2];
bool ink = glyphWidth == 16 && renderWidth == 8 ? (row & (3 << (14 - (sx - dx) * 2))) != 0 : (row & (1 << (15 - (sx - dx)))) != 0;
alpha = ink ? (byte)255 : (byte)0;
}
else alpha = 0;
int px = left + x, py = top + y;
if (alpha != 0 && px >= 0 && py >= 0 && px < Width && py < Height)
pixels[py * Width + px] = (int)Blend(color, (uint)pixels[py * Width + px], alpha);
}
}
private static int UnicodeOffset(int character) => character switch
{
>= 128 and <= 2047 => (character - 128) * 33,
>= 8192 and <= 8303 => 63360 + (character - 8192) * 33,
>= 8592 and <= 8703 => 67056 + (character - 8592) * 33,
>= 12288 and <= 12351 => 70752 + (character - 12288) * 33,
>= 13312 and <= 40959 => 72864 + (character - 13312) * 33,
>= 65280 and <= 65519 => 985248 + (character - 65280) * 33,
_ => 993168
};
private static bool IsWide(int value) => value is >= 0x1100 and <= 0x115F or 0x2329 or 0x232A or >= 0x2E80 and <= 0xA4CF or
>= 0xAC00 and <= 0xD7A3 or >= 0xF900 and <= 0xFAFF or >= 0xFE10 and <= 0xFE19 or >= 0xFE30 and <= 0xFE6F or
>= 0xFF00 and <= 0xFF60 or >= 0xFFE0 and <= 0xFFE6 or >= 0x1F300 and <= 0x1FAFF or >= 0x20000 and <= 0x3FFFD;
private static uint Color(TerminalColor color) => color == TerminalColor.ThemeBackground ? Background : (uint)(color.Red << 16 | color.Green << 8 | color.Blue);
private static uint Blend(uint foreground, uint background, byte alpha)
{
uint result = 0;
for (int shift = 0; shift <= 16; shift += 8)
result |= ((((foreground >> shift & 255) * alpha + (background >> shift & 255) * (uint)(255 - alpha) + 127) / 255) << shift);
return result;
}
private void Pixel(int x, int y, uint color)
{ if (x >= 0 && x < Width && y >= 0 && y < Height) pixels[y * Width + x] = (int)color; }
private void Fill(uint color, int x, int y, int width, int height)
{
int right = Math.Min(Width, x + width), bottom = Math.Min(Height, y + height);
for (int row = Math.Max(0, y); row < bottom; row++)
if (right > Math.Max(0, x)) pixels.AsSpan(row * Width + Math.Max(0, x), right - Math.Max(0, x)).Fill((int)color);
}
private static byte[] ReadAsset(string name, int length)
{
using var stream = typeof(ClassicConsoleRenderer).Assembly.GetManifestResourceStream("ConsoleAssets/" + name)
?? throw new InvalidDataException("Missing classic console asset: " + name);
if (stream.Length != length) throw new InvalidDataException("Invalid classic console asset: " + name);
byte[] result = new byte[length]; stream.ReadExactly(result); return result;
}
}