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(); } }