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

XFESystem

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

公开
关注 0 Fork 0 Star 0
UTF-8
using System.Buffers.Binary;
using System.IO.Compression;
using XFESystem.Desktop;
using Xunit;

namespace XFESystem.Core.Tests;

public sealed class WindowChromeRasterTests
{
    private static readonly DesktopBounds Window = new(20, 16, 360, 220);
    private sealed class Bitmap(int scale, uint background) : IDesktopCoverageCanvas
    {
        public int Width => 420;
        public int Height => 280;
        public int NativeWidth => Width * scale;
        public int NativeHeight => Height * scale;
        public int ScalePercent => scale * 100;
        public uint[] Pixels { get; } = Enumerable.Repeat(background, 420 * 280 * scale * scale).ToArray();
        public void Fill(uint rgb, int x, int y, int width, int height)
        {
            for (int row = Math.Max(0, y * scale); row < Math.Min(NativeHeight, (y + height) * scale); row++)
                for (int col = Math.Max(0, x * scale); col < Math.Min(NativeWidth, (x + width) * scale); col++) Pixels[row * NativeWidth + col] = rgb;
        }
        public void BlendCoverage(uint rgb, int x, int y, int width, int height, ReadOnlySpan<byte> mask)
        {
            Assert.Equal(width * height, mask.Length);
            for (int row = Math.Max(0, y); row < Math.Min(NativeHeight, y + height); row++)
                for (int col = Math.Max(0, x); col < Math.Min(NativeWidth, x + width); col++)
                {
                    byte alpha = mask[(row - y) * width + col - x];
                    if (alpha != 0) Pixels[row * NativeWidth + col] = Mix(Pixels[row * NativeWidth + col], rgb, alpha);
                }
        }
        public uint At(double x, double y) => Pixels[(int)(y * scale) * NativeWidth + (int)(x * scale)];
        public void Line(uint rgb, int x1, int y1, int x2, int y2) => throw new InvalidOperationException("Native strokes must use alpha coverage.");
        public void Text(string text, uint rgb, int x, int y) { }
        public void Present() { }
    }

    private static uint Mix(uint background, uint color, int alpha)
    {
        uint result = 0;
        for (int shift = 0; shift <= 16; shift += 8)
            result |= (uint)(((color >> shift & 255) * alpha + (background >> shift & 255) * (255 - alpha) + 127) / 255) << shift;
        return result;
    }
    private static Bitmap Render(int scale, DesktopTheme theme, double min = 0, double close = 0,
        double title = 0, double resize = 0, bool disabledResize = false)
    {
        var canvas = new Bitmap(scale, theme.Panel);
        DesktopWindowChromeRenderer.Draw(canvas, Window, theme, true, min, close, title, resize, disabledResize);
        return canvas;
    }
    private static uint[] Region(Bitmap canvas, DesktopBounds bounds)
    {
        int scale = canvas.ScalePercent / 100;
        return Enumerable.Range(bounds.Y * scale, bounds.Height * scale)
            .SelectMany(row => canvas.Pixels.AsSpan(row * canvas.NativeWidth + bounds.X * scale, bounds.Width * scale).ToArray()).ToArray();
    }

    [Theory]
    [InlineData(1, DesktopThemeMode.Light)]
    [InlineData(2, DesktopThemeMode.Light)]
    [InlineData(1, DesktopThemeMode.Dark)]
    [InlineData(2, DesktopThemeMode.Dark)]
    public void CaptionControlsShareCenterlineAndCloseHasPermanentWhiteCircle(int scale, DesktopThemeMode mode)
    {
        var theme = new DesktopTheme(mode);
        var canvas = Render(scale, theme);
        double right = Window.X + Window.Width, cy = Window.Y + 16;
        Assert.Equal(0xFFFFFFu, canvas.At(right - 16, cy - 8));
        Assert.Equal(0xFFFFFFu, canvas.At(right - 16, cy + 8));
        Assert.NotEqual(theme.Panel, canvas.At(right - 48, cy));
        Assert.Equal(theme.Panel, canvas.At(right - 48, cy - 7));
        Assert.Equal(theme.Panel, canvas.At(right - 48, cy + 7));
        Assert.Contains(Region(canvas, DesktopWindowChromeRenderer.CaptionDamage(Window, true)),
            value => value != theme.Panel && value != 0xFFFFFF && value != theme.AccentStroke);
    }

    [Theory]
    [InlineData(1)]
    [InlineData(2)]
    public void CaptionIntermediateFramesDifferFromBothEndpoints(int scale)
    {
        var theme = new DesktopTheme(DesktopThemeMode.Dark);
        var idle = Render(scale, theme);
        var middle = Render(scale, theme, .25, .25);
        var complete = Render(scale, theme, 1, 1);
        var minBounds = DesktopWindowChromeRenderer.CaptionDamage(Window, false);
        var closeBounds = DesktopWindowChromeRenderer.CaptionDamage(Window, true);
        Assert.False(Region(idle, minBounds).SequenceEqual(Region(middle, minBounds)));
        Assert.False(Region(complete, minBounds).SequenceEqual(Region(middle, minBounds)));
        Assert.False(Region(idle, closeBounds).SequenceEqual(Region(middle, closeBounds)));
        Assert.True(Region(idle, closeBounds).SequenceEqual(Region(complete, closeBounds))); // Full 180-degree X symmetry.
        Assert.Equal(theme.Panel, complete.At(Window.X + Window.Width - 54, Window.Y + 16));
        Assert.NotEqual(theme.Panel, complete.At(Window.X + Window.Width - 48, Window.Y + 21));
    }

    [Fact]
    public void CaptionInkRemainsVisibleInsideWhiteCircleForCustomWhiteDarkAccent()
    {
        var theme = new DesktopTheme(DesktopThemeMode.Dark, 0xFFFFFF);
        var canvas = Render(2, theme);
        uint center = canvas.At(Window.X + Window.Width - 16, Window.Y + 16);
        Assert.True(DesktopTheme.Contrast(center, 0xFFFFFF) >= 3);
    }

    [Theory]
    [InlineData(1)]
    [InlineData(2)]
    public void TitleDoubleBarsAndBothCaptionControlsShareTheSameCenterline(int scale)
    {
        var theme = new DesktopTheme(DesktopThemeMode.Dark);
        var canvas = Render(scale, theme);
        double CenterOfInk(DesktopBounds bounds)
        {
            double yTotal = 0; int count = 0;
            for (int row = bounds.Y * scale; row < (bounds.Y + bounds.Height) * scale; row++)
                for (int col = bounds.X * scale; col < (bounds.X + bounds.Width) * scale; col++)
                    if (canvas.Pixels[row * canvas.NativeWidth + col] != theme.Panel)
                    { yTotal += (row + .5) / scale; count++; }
            Assert.True(count > 0);
            return yTotal / count;
        }
        Assert.Equal(Window.Y + 16, CenterOfInk(DesktopWindowChromeRenderer.TitleDamage(Window)), 5);
        Assert.Equal(Window.Y + 16, CenterOfInk(DesktopWindowChromeRenderer.CaptionDamage(Window, false)), 5);
        Assert.Equal(Window.Y + 16, CenterOfInk(DesktopWindowChromeRenderer.CaptionDamage(Window, true)), 5);
    }

    [Theory]
    [InlineData(1)]
    [InlineData(2)]
    public void TitleAndResizePanelsFadeAgainstActualBackgroundWithoutOpaqueWhiteTiles(int scale)
    {
        var theme = new DesktopTheme(DesktopThemeMode.Dark);
        var idle = Render(scale, theme);
        var half = Render(scale, theme, title: .5, resize: .5);
        var full = Render(scale, theme, title: 1, resize: 1);
        double cx = Window.X + Window.Width / 2d;
        Assert.Equal(theme.Panel, idle.At(cx + 25, Window.Y + 20));
        Assert.Equal(Mix(theme.Panel, 0xFFFFFF, 20), half.At(cx + 25, Window.Y + 20));
        Assert.Equal(Mix(theme.Panel, 0xFFFFFF, 40), full.At(cx + 25, Window.Y + 20));
        double right = Window.X + Window.Width, bottom = Window.Y + Window.Height;
        Assert.Equal(Mix(theme.Panel, 0xFFFFFF, 44), full.At(right - 13, bottom - 13));
        Assert.NotEqual(0xFFFFFFu, full.At(right - 13, bottom - 13));
        Assert.Equal(theme.Panel, full.At(right - 30, bottom - 15));
        Assert.True(Region(full, DesktopWindowChromeRenderer.TitleDamage(Window)).SequenceEqual(
            Region(Render(scale, theme, title: 1, resize: 1), DesktopWindowChromeRenderer.TitleDamage(Window))));
    }

    [Theory]
    [InlineData(1)]
    [InlineData(2)]
    public void DefaultResizeAffordanceFollowsQuarterRadiusAndDisablesForMaximizedWindow(int scale)
    {
        var theme = new DesktopTheme(DesktopThemeMode.Dark);
        var normal = Render(scale, theme);
        var maximized = Render(scale, theme, disabledResize: true);
        var bounds = DesktopWindowChromeRenderer.ResizeDamage(Window);
        Assert.Contains(Region(normal, bounds), value => value != theme.Panel);
        Assert.All(Region(maximized, bounds), value => Assert.Equal(theme.Panel, value));
        double centerX = Window.X + Window.Width - 18, centerY = Window.Y + Window.Height - 18;
        for (int row = bounds.Y * scale; row < (bounds.Y + bounds.Height) * scale; row++)
            for (int col = bounds.X * scale; col < (bounds.X + bounds.Width) * scale; col++)
            {
                if (normal.Pixels[row * normal.NativeWidth + col] == theme.Panel) continue;
                double x = (col + .5) / scale - centerX, y = (row + .5) / scale - centerY;
                Assert.InRange(Math.Sqrt(x * x + y * y), 13.5, 17.5);
            }
    }

    [Fact]
    public void AnimationChangesStayInsideDeclaredSmallDamageRegions()
    {
        var theme = new DesktopTheme(DesktopThemeMode.Dark);
        var idle = Render(2, theme);
        var active = Render(2, theme, .4, .3, .7, .8);
        var regions = new[] { DesktopWindowChromeRenderer.TitleDamage(Window),
            DesktopWindowChromeRenderer.CaptionDamage(Window, false), DesktopWindowChromeRenderer.CaptionDamage(Window, true),
            DesktopWindowChromeRenderer.ResizeDamage(Window) };
        Assert.InRange(regions[0].Width, Window.Width * 2 / 3, Window.Width * 2 / 3 + 2);
        for (int row = 0; row < active.NativeHeight; row++)
            for (int col = 0; col < active.NativeWidth; col++)
                if (active.Pixels[row * active.NativeWidth + col] != idle.Pixels[row * active.NativeWidth + col])
                    Assert.Contains(regions, region => region.Contains(col / 2, row / 2));
    }

    [Fact]
    public void PressedFeedbackChangesOnlyCaptionGlyphsAndCircleWithoutSquareTiles()
    {
        var theme = new DesktopTheme(DesktopThemeMode.Dark);
        var idle = Render(2, theme); var pressed = new Bitmap(2, theme.Panel);
        DesktopWindowChromeRenderer.Draw(pressed, Window, theme, true, 0, 0, 0, 0,
            minimizePressed: true, closePressed: true);
        var minimize = DesktopWindowChromeRenderer.CaptionDamage(Window, false);
        var close = DesktopWindowChromeRenderer.CaptionDamage(Window, true);
        Assert.False(Region(idle, minimize).SequenceEqual(Region(pressed, minimize)));
        Assert.False(Region(idle, close).SequenceEqual(Region(pressed, close)));
        Assert.DoesNotContain(0xFFFFFFu, Region(pressed, minimize));
        for (int row = 0; row < pressed.NativeHeight; row++)
            for (int col = 0; col < pressed.NativeWidth; col++)
                if (pressed.Pixels[row * pressed.NativeWidth + col] != idle.Pixels[row * idle.NativeWidth + col])
                    Assert.True(minimize.Contains(col / 2, row / 2) || close.Contains(col / 2, row / 2));
    }

    [Fact]
    public void LightHoverPanelHasSubtleThemeEdgeWithoutChangingItsAreaOrWhiteInterior()
    {
        var theme = new DesktopTheme(DesktopThemeMode.Light);
        var idle = Render(2, theme); var hover = Render(2, theme, title: 1);
        double center = Window.X + Window.Width / 2d;
        double left = center - (Window.Width * 2 / 3) / 2d;
        uint edge = hover.At(left, Window.Y + 10);
        Assert.True((idle.At(left, Window.Y + 10) >> 16 & 255) - (edge >> 16 & 255) > 5);
        Assert.Equal(Mix(theme.Panel, 0xFFFFFF, 40), hover.At(center + 25, Window.Y + 20));
        var damage = DesktopWindowChromeRenderer.TitleDamage(Window);
        for (int row = 0; row < hover.NativeHeight; row++)
            for (int col = 0; col < hover.NativeWidth; col++)
                if (hover.Pixels[row * hover.NativeWidth + col] != idle.Pixels[row * idle.NativeWidth + col])
                    Assert.True(damage.Contains(col / 2, row / 2));
    }

    [Fact]
    public void RotatedTwoHundredPercentStrokeIsRasterizedAtNativePixelPrecision()
    {
        var theme = new DesktopTheme(DesktopThemeMode.Dark);
        var frame = Render(2, theme, min: .25);
        var bounds = DesktopWindowChromeRenderer.CaptionDamage(Window, false);
        int varied = 0;
        for (int row = bounds.Y * 2; row < (bounds.Y + bounds.Height) * 2; row += 2)
            for (int col = bounds.X * 2; col < (bounds.X + bounds.Width) * 2; col += 2)
            {
                uint value = frame.Pixels[row * frame.NativeWidth + col];
                if (value != frame.Pixels[row * frame.NativeWidth + col + 1] ||
                    value != frame.Pixels[(row + 1) * frame.NativeWidth + col] ||
                    value != frame.Pixels[(row + 1) * frame.NativeWidth + col + 1]) varied++;
            }
        Assert.True(varied > 12);
    }

    private sealed class Probe : IDesktopCoverageCanvas
    {
        public int Width => 420;
        public int Height => 280;
        public int ScalePercent => 200;
        public void Fill(uint rgb, int x, int y, int width, int height) { }
        public void BlendCoverage(uint rgb, int x, int y, int width, int height, ReadOnlySpan<byte> mask) { }
        public void Line(uint rgb, int x1, int y1, int x2, int y2) => throw new InvalidOperationException();
        public void Text(string text, uint rgb, int x, int y) { }
        public void Present() { }
    }

    [Fact]
    public void WarmQuantizedAnimationDoesNotAllocatePerFrameMasks()
    {
        var canvas = new Probe(); var theme = new DesktopTheme();
        for (int pass = 0; pass < 2; pass++)
            for (int step = 0; step <= DesktopWindowChromeRenderer.AnimationSteps; step++)
                DesktopWindowChromeRenderer.Draw(canvas, Window, theme, true, step / 32d, step / 32d, step / 32d, step / 32d);
        long before = GC.GetAllocatedBytesForCurrentThread();
        for (int frame = 0; frame < 200; frame++)
        {
            double phase = frame % 33 / 32d;
            DesktopWindowChromeRenderer.Draw(canvas, Window, theme, true, phase, phase, phase, phase);
        }
        Assert.Equal(0, GC.GetAllocatedBytesForCurrentThread() - before);
    }

    [Fact]
    public void OptionalNativeInspectionPreviewUsesProductionRenderer()
    {
        string? directory = Environment.GetEnvironmentVariable("XFE_CHROME_PROOF_PATH");
        if (directory is null) return;
        Assert.True(Path.IsPathFullyQualified(directory)); Directory.CreateDirectory(directory);
        foreach (int scale in new[] { 1, 2 })
            foreach (DesktopThemeMode mode in Enum.GetValues<DesktopThemeMode>())
                foreach (int step in new[] { 0, 8, 32 })
                {
                    var bitmap = Render(scale, new DesktopTheme(mode), step / 32d, step / 32d, step / 32d, step / 32d);
                    WritePng(Path.Combine(directory, $"Chrome-{mode}-{scale}x-{step}.png"), bitmap.NativeWidth, bitmap.NativeHeight, bitmap.Pixels);
                }
    }

    private static void WritePng(string path, int width, int height, uint[] pixels)
    {
        using var output = File.Create(path); output.Write(new byte[] { 137, 80, 78, 71, 13, 10, 26, 10 });
        Span<byte> header = stackalloc byte[13]; header.Clear();
        BinaryPrimitives.WriteUInt32BigEndian(header, (uint)width); BinaryPrimitives.WriteUInt32BigEndian(header[4..], (uint)height);
        header[8] = 8; header[9] = 2; Chunk(output, "IHDR"u8, header);
        using var compressed = new MemoryStream();
        using (var zlib = new ZLibStream(compressed, CompressionLevel.Fastest, true))
            for (int row = 0; row < height; row++)
            {
                zlib.WriteByte(0);
                for (int col = 0; col < width; col++)
                {
                    uint rgb = pixels[row * width + col];
                    zlib.WriteByte((byte)(rgb >> 16)); zlib.WriteByte((byte)(rgb >> 8)); zlib.WriteByte((byte)rgb);
                }
            }
        Chunk(output, "IDAT"u8, compressed.ToArray()); Chunk(output, "IEND"u8, []);
    }
    private static void Chunk(Stream output, ReadOnlySpan<byte> name, ReadOnlySpan<byte> bytes)
    {
        Span<byte> word = stackalloc byte[4]; BinaryPrimitives.WriteUInt32BigEndian(word, (uint)bytes.Length);
        output.Write(word); output.Write(name); output.Write(bytes); uint crc = uint.MaxValue;
        foreach (byte value in name) crc = Crc(crc, value);
        foreach (byte value in bytes) crc = Crc(crc, value);
        BinaryPrimitives.WriteUInt32BigEndian(word, ~crc); output.Write(word);
    }
    private static uint Crc(uint crc, byte value)
    {
        crc ^= value;
        for (int bit = 0; bit < 8; bit++) crc = crc >> 1 ^ ((crc & 1) == 0 ? 0 : 0xEDB88320u);
        return crc;
    }
}