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 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 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()) 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 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 name, ReadOnlySpan bytes) { Span 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; } }