XFE Git
XFE Studio Git
Git 首页 全局搜索
XFE 主站 文档 NuGet

XFESystem

【Rust】XFE系统,基于Rust和汇编

公开
关注 0 Fork 0 Star 0
UTF-8
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;
    }
}