The client shipped with a PE icon Explorer showed and a window that did not:
launched from the launcher it still drew the stock Windows application icon.
Silk's Window.Create only builds the managed object. IWindow.Initialize is
what, in Silk's own words, "creates the window on the underlying platform".
Applying an icon before that throws:
after Window.Create : IsInitialized = False
SetWindowIcon BEFORE Initialize : THREW InvalidOperationException:
Window should be initialized.
after Initialize : IsInitialized = True
SetWindowIcon AFTER Initialize : returned without throwing
What made this quiet rather than obvious is the fallback. GLFW registers its
window class against a resource named GLFW_ICON and, not finding one, uses
IDI_APPLICATION - the generic Windows icon - rather than the executable's own.
So the PE icon kept showing on the file while the live window lost it, which
reads as a packaging problem and is nothing of the kind. The launcher was
unaffected because Avalonia takes a different path entirely, and that
asymmetry was the tell.
Apply now happens in OnLoad, beside the other window-dependent startup work,
and refuses with a message naming the ordering requirement if it is ever
called on an uninitialized window - the previous generic catch reported
"Window should be initialized" to a stderr nobody reads, which said nothing
about icons.
The regression guard reads the compiled call graph, because this is an
ordering edge with no observable return value: OnLoad must call Apply, and no
method that calls Window.Create may. Verified by reintroducing the bug and
watching it fail, then restoring the fix and watching it pass.
Solution builds clean; 14,408 tests pass on the standard hermetic lane filter,
0 failures.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
128 lines
4.9 KiB
C#
128 lines
4.9 KiB
C#
using Silk.NET.Core;
|
|
using Silk.NET.Windowing;
|
|
using SixLabors.ImageSharp;
|
|
using SixLabors.ImageSharp.PixelFormats;
|
|
|
|
namespace AcDream.App.Rendering;
|
|
|
|
/// <summary>
|
|
/// Applies acdream's window icon to the native window.
|
|
/// </summary>
|
|
/// <remarks>
|
|
/// <para>
|
|
/// The icon art is the retail mosswart head (Setup <c>0x02000B4F</c> part 14,
|
|
/// skin <c>0x05001E11</c>) rendered offline by <c>tools/IconForge</c>; only the
|
|
/// baked PNGs ship. See <c>assets/icons/README.md</c>.
|
|
/// </para>
|
|
/// <para>
|
|
/// The PNGs are <b>embedded</b> 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.
|
|
/// </para>
|
|
/// <para>
|
|
/// 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.
|
|
/// </para>
|
|
/// </remarks>
|
|
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",
|
|
};
|
|
|
|
/// <summary>
|
|
/// Decode the embedded icon set and hand it to the window.
|
|
/// </summary>
|
|
/// <param name="window">
|
|
/// A window that has already been initialized. Silk's <c>Window.Create</c>
|
|
/// returns an uninitialized object — <c>IWindow.Initialize</c> is what
|
|
/// "creates the window on the underlying platform" — so this must be called
|
|
/// from <c>Load</c> or later.
|
|
/// </param>
|
|
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<RawImage>(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<Rgba32>(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();
|
|
}
|
|
}
|