using Silk.NET.Core;
using Silk.NET.Windowing;
using SixLabors.ImageSharp;
using SixLabors.ImageSharp.PixelFormats;
namespace AcDream.App.Rendering;
///
/// Applies acdream's window icon to the native window.
///
///
///
/// The icon art is the retail mosswart head (Setup 0x02000B4F part 14,
/// skin 0x05001E11) rendered offline by tools/IconForge; only the
/// baked PNGs ship. See assets/icons/README.md.
///
///
/// The PNGs are embedded rather than copied next to the binary, unlike
/// the shader/markup assets in this project. Two reasons: a window icon has no
/// sensible runtime fallback if the file is missing, and embedding keeps it
/// intact under single-file publish. The cost is a few tens of KB in the
/// assembly.
///
///
/// Several sizes are handed over at once because the window manager picks the
/// closest match per surface — the title bar wants ~16px while Alt-Tab and the
/// taskbar want 32-256px, and letting the WM choose beats shipping one size and
/// having it resampled badly.
///
///
internal static class WindowIconLoader
{
// Ordered small -> large purely for readability; the window manager selects
// by size, not by position.
private static readonly string[] ResourceNames =
{
"AcDream.App.Rendering.Icons.acdream-client-16.png",
"AcDream.App.Rendering.Icons.acdream-client-32.png",
"AcDream.App.Rendering.Icons.acdream-client-48.png",
"AcDream.App.Rendering.Icons.acdream-client-256.png",
};
///
/// Decode the embedded icon set and hand it to the window.
///
///
/// A window that has already been initialized. Silk's Window.Create
/// returns an uninitialized object — IWindow.Initialize is what
/// "creates the window on the underlying platform" — so this must be called
/// from Load or later.
///
public static void Apply(IWindow window)
{
ArgumentNullException.ThrowIfNull(window);
// Named explicitly rather than left to the catch below. Calling too
// early throws a generic InvalidOperationException whose message says
// nothing about icons, and the symptom — GLFW falling back to the
// stock Windows application icon while the executable's own PE icon
// still shows in Explorer — looks like a packaging problem rather than
// an ordering one. That misdirection already cost one shipped build.
if (!window.IsInitialized)
{
Console.Error.WriteLine(
"window icon: refusing to set an icon on an uninitialized window — "
+ "call WindowIconLoader.Apply from the Load callback, not next to "
+ "Window.Create.");
return;
}
RawImage[] images;
try
{
images = Decode();
}
catch (Exception failure)
{
// A missing or corrupt embedded resource is a build defect, not a
// runtime condition — say so loudly rather than shipping a silent
// catch, but do not take the client down over cosmetics.
Console.Error.WriteLine($"window icon: could not decode embedded icons — {failure}");
return;
}
if (images.Length == 0)
{
Console.Error.WriteLine("window icon: no embedded icon resources found");
return;
}
try
{
window.SetWindowIcon(images.AsSpan());
}
catch (Exception failure)
{
// Wayland has no window-icon protocol and GLFW reports the request
// as unsupported there. That is a platform fact, not a bug, and it
// must not be fatal — but it is still worth printing so an
// unexpectedly icon-less window on a supported platform is
// traceable rather than mysterious.
Console.Error.WriteLine($"window icon: platform rejected the icon — {failure.Message}");
}
}
private static RawImage[] Decode()
{
var assembly = typeof(WindowIconLoader).Assembly;
var decoded = new List(ResourceNames.Length);
foreach (string name in ResourceNames)
{
using Stream? stream = assembly.GetManifestResourceStream(name);
if (stream is null)
{
Console.Error.WriteLine($"window icon: embedded resource missing — {name}");
continue;
}
using var image = Image.Load(stream);
var pixels = new byte[image.Width * image.Height * 4];
image.CopyPixelDataTo(pixels);
decoded.Add(new RawImage(image.Width, image.Height, pixels));
}
return decoded.ToArray();
}
}