Skip to content

Commit 8d67d73

Browse files
committed
Merge branch 'windows_icon_in_bytes' #310 #135
2 parents 4f2f175 + e2b62b3 commit 8d67d73

12 files changed

Lines changed: 202 additions & 41 deletions

File tree

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
# WIP
22

33
- Bumped types in pom.xml to 0.2.0
4+
- Windows, Linux: Window::setIconPixels (previously setIconData on Linux) #310 #135 via @chirontt
45

56
# 0.4.25 - Jun 16, 2026
67

examples/dashboard/java/Example.java

Lines changed: 39 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,19 @@ public class Example implements Consumer<Event> {
4242

4343
public Options progressBars = new Options("Default", "0%", "50%", "100%", "Indeterminate");
4444

45+
// Icons to cycle through with Ctrl+I. .ico/.icns are loaded as files, .png as raw image data.
46+
public Options icons = switch (Platform.CURRENT) {
47+
case WINDOWS -> new Options("examples/dashboard/resources/windows.ico",
48+
"windows/icon_16x16.png",
49+
"windows/icon_24x24.png",
50+
"windows/icon_32x32.png",
51+
"windows/icon_48x48.png",
52+
"windows/icon_256x256.png");
53+
case MACOS -> new Options("examples/dashboard/resources/macos.icns");
54+
case X11 -> new Options("linux/icon_24x24.png",
55+
"linux/icon_48x48.png");
56+
};
57+
4558
public Example() {
4659
window = App.makeWindow();
4760
window.setEventListener(this);
@@ -54,7 +67,7 @@ public Example() {
5467
panelMouseCursors = new PanelMouseCursors(window);
5568
panelRendering = new PanelRendering(window);
5669
panelEvents = new PanelEvents(window);
57-
panelTheme = new PanelTheme(window);
70+
panelTheme = new PanelTheme(window, icons);
5871
panelTouch = new PanelTouch(window);
5972

6073
var scale = window.getScreen().getScale();
@@ -79,26 +92,11 @@ public Example() {
7992
case 4 -> window.setWindowPosition(bounds.getLeft() + bounds.getWidth() / 2, bounds.getTop() + bounds.getHeight() / 2);
8093
}
8194

82-
var classLoader = getClass().getClassLoader();
83-
switch (Platform.CURRENT) {
84-
case WINDOWS -> {
85-
window.setIcon(new File("examples/dashboard/resources/windows.ico"));
86-
}
87-
case MACOS -> {
88-
window.setIcon(new File("examples/dashboard/resources/macos.icns"));
89-
}
90-
case X11 -> {
91-
((WindowX11) window).setClassHint("jwm-dashboard-example"); // allows OS-wide identification of the window (e.g. icon themes, .desktop files)
92-
try (var in = classLoader.getResourceAsStream("linux/icon_48x48.png")) {
93-
Bitmap i = Bitmap.makeFromImage(Image.makeDeferredFromEncodedBytes(in.readAllBytes()));
94-
ImageInfo info = i.getImageInfo();
95-
96-
((WindowX11) window).setIconData(info.getWidth(), info.getHeight(), i.readPixels());
97-
} catch (IOException e) {
98-
e.printStackTrace();
99-
}
100-
}
95+
if (window instanceof WindowX11 windowX11) {
96+
// allows OS-wide identification of the window (e.g. icon themes, .desktop files)
97+
windowX11.setClassHint("jwm-dashboard-example");
10198
}
99+
applyIcon();
102100

103101
window.setVisible(true);
104102
initialized = true;
@@ -215,6 +213,10 @@ public void accept(Event e) {
215213
window.minimize();
216214
case B ->
217215
setProgressBar(progressBars.next());
216+
case I -> {
217+
icons.next();
218+
applyIcon();
219+
}
218220
case A -> {
219221
if (window instanceof WindowMac windowMac) {
220222
boolean enabled = !windowMac.isPressAndHoldEnabled();
@@ -235,6 +237,23 @@ public void accept(Event e) {
235237
}
236238
}
237239

240+
public void applyIcon() {
241+
String path = icons.get();
242+
if (path.endsWith(".png")) {
243+
try (var in = getClass().getClassLoader().getResourceAsStream(path);
244+
var img = Image.makeDeferredFromEncodedBytes(in.readAllBytes());
245+
var bitmap = Bitmap.makeFromImage(img); )
246+
{
247+
ImageInfo info = bitmap.getImageInfo();
248+
window.setIconPixels(info.getWidth(), info.getHeight(), bitmap.readPixels());
249+
} catch (IOException e) {
250+
e.printStackTrace();
251+
}
252+
} else {
253+
window.setIcon(new File(path));
254+
}
255+
}
256+
238257
public void setProgressBar(String type) {
239258
progressBars.set(type);
240259
window.setProgressBar(switch (type) {

examples/dashboard/java/PanelLegend.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ public PanelLegend(Window window) {
2222
shortcuts.put("U", "Lock mouse cursor");
2323
shortcuts.put("Z", "Toggle Z-order");
2424
shortcuts.put("B", "Toggle Progress Bar");
25+
shortcuts.put("I", "Toggle icon");
2526
shortcuts.put("O", "Opacity");
2627
shortcuts.put("1", "Minimize");
2728
shortcuts.put("2", "Maximize");

examples/dashboard/java/PanelTheme.java

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,11 @@
55
import io.github.humbleui.skija.*;
66

77
public class PanelTheme extends Panel {
8+
public Options icons;
89

9-
public PanelTheme(Window window) {
10+
public PanelTheme(Window window, Options icons) {
1011
super(window);
12+
this.icons = icons;
1113
}
1214

1315
@Override
@@ -60,6 +62,11 @@ public void paintImpl(Canvas canvas, int width, int height, float scale) {
6062

6163
canvas.drawString("zOrder", Example.PADDING, Example.PADDING * 12, Example.FONT12, paint);
6264
canvas.drawString("" + window.getZOrder(), width / 2 + Example.PADDING / 2, Example.PADDING * 12, Example.FONT12, paint);
65+
66+
String icon = icons.get();
67+
icon = icon.substring(icon.lastIndexOf('/') + 1);
68+
canvas.drawString("icon", Example.PADDING, Example.PADDING * 14, Example.FONT12, paint);
69+
canvas.drawString(icon, width / 2 + Example.PADDING / 2, Example.PADDING * 14, Example.FONT12, paint);
6370
}
6471
}
6572
}

linux/cc/WindowX11.cc

Lines changed: 6 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@ void WindowX11::setClass(const std::string& name, const std::string& appClass) {
4242
}
4343
}
4444

45-
void WindowX11::setIconData(int width, int height, const unsigned char* argb) {
45+
void WindowX11::setIconPixels(int width, int height, const unsigned char* argb) {
4646
size_t size = width * height;
4747
size_t count = size + 2;
4848
std::unique_ptr<long[]> buffer{new long[count]};
@@ -609,13 +609,12 @@ extern "C" JNIEXPORT void JNICALL Java_io_github_humbleui_jwm_WindowX11__1nSetCl
609609
instance->setClass(bytesToString(env, name), bytesToString(env, appClass));
610610
}
611611

612-
extern "C" JNIEXPORT void JNICALL Java_io_github_humbleui_jwm_WindowX11__1nSetIconData
613-
(JNIEnv* env, jobject obj, jint width, jint height, jbyteArray data) {
612+
extern "C" JNIEXPORT void JNICALL Java_io_github_humbleui_jwm_WindowX11__1nSetIconPixels
613+
(JNIEnv* env, jobject obj, jint width, jint height, jbyteArray pixelsArr) {
614614
jwm::WindowX11* instance = reinterpret_cast<jwm::WindowX11*>(jwm::classes::Native::fromJava(env, obj));
615-
616-
jbyte* bytes = env->GetByteArrayElements(data, nullptr);
617-
instance->setIconData(width, height, reinterpret_cast<const unsigned char*>(bytes));
618-
env->ReleaseByteArrayElements(data, bytes, 0);
615+
jbyte* pixels = env->GetByteArrayElements(pixelsArr, nullptr);
616+
instance->setIconPixels(width, height, reinterpret_cast<const unsigned char*>(pixels));
617+
env->ReleaseByteArrayElements(pixelsArr, pixels, 0);
619618
}
620619

621620
extern "C" JNIEXPORT void JNICALL Java_io_github_humbleui_jwm_WindowX11__1nSetTitlebarVisible

linux/cc/WindowX11.hh

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@ namespace jwm {
3939
}
4040
void setTitle(const std::string& title);
4141
void setClass(const std::string& name, const std::string& class_);
42-
void setIconData(int width, int height, const unsigned char* argb);
42+
void setIconPixels(int width, int height, const unsigned char* argb);
4343
void setTitlebarVisible(bool isVisible);
4444

4545
void maximize();

linux/java/WindowX11.java

Lines changed: 10 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -98,18 +98,19 @@ public Window setClassHint(String name, String appClass) {
9898
/**
9999
* <p>Set window icon from raw image bytes.</p>
100100
*
101-
* <p>{@code data} must have a length of {@code width * height * 4}, representing per-pixel ARGB data.</p>
101+
* <p>{@code pixels} must have a length of {@code width * height * 4}, representing per-pixel ARGB data.</p>
102102
*
103-
* @param width icon width in pixels
104-
* @param height icon height in pixels
105-
* @param data icon image data
106-
* @return this
103+
* @param width icon width in pixels
104+
* @param height icon height in pixels
105+
* @param pixels icon image data
106+
* @return this
107107
*/
108+
@Override
108109
@NotNull @Contract("-> this")
109-
public Window setIconData(int width, int height, byte[] data) {
110+
public Window setIconPixels(int width, int height, byte[] pixels) {
110111
assert _onUIThread() : "Should be run on UI thread";
111-
assert data.length == width*height*4 : "Incorrect icon data array length";
112-
_nSetIconData(width, height, data);
112+
assert pixels.length == width * height * 4 : "Incorrect icon data array length";
113+
_nSetIconPixels(width, height, pixels);
113114
return this;
114115
}
115116

@@ -264,7 +265,7 @@ public boolean isFullScreen() {
264265
@ApiStatus.Internal public native void _nRestore();
265266
@ApiStatus.Internal public native void _nSetTitle(byte[] title);
266267
@ApiStatus.Internal public native void _nSetClassHint(byte[] name, byte[] appClass);
267-
@ApiStatus.Internal public native void _nSetIconData(int width, int height, byte[] data);
268+
@ApiStatus.Internal public native void _nSetIconPixels(int width, int height, byte[] pixels);
268269
@ApiStatus.Internal public native void _nSetTitlebarVisible(boolean isVisible);
269270
@ApiStatus.Internal public native void _nSetFullScreen(boolean isFullScreen);
270271
@ApiStatus.Internal public native boolean _nIsFullScreen();

macos/java/WindowMac.java

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -109,6 +109,12 @@ public Window setIcon(File icon) {
109109
return this;
110110
}
111111

112+
@Override
113+
public Window setIconPixels(int width, int height, byte[] pixels) {
114+
// Not implemented
115+
return this;
116+
}
117+
112118
/**
113119
* <p>Shortcut for {@link #setTitleVisible(boolean)}, {@link #setFullSizeContentView(boolean)}</p>
114120
*

shared/java/Window.java

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -204,6 +204,21 @@ public IRect getContentRectAbsolute() {
204204
@NotNull @Contract("-> this")
205205
public abstract Window setIcon(File icon);
206206

207+
/**
208+
* <p>Set window icon from raw image bytes.</p>
209+
*
210+
* <p>{@code pixels} must have a length of {@code width * height * 4}, representing per-pixel ARGB data.</p>
211+
*
212+
* <p>Displayed in the top-bar and task switcher on Linux and Windows. No-op on macOS.</p>
213+
*
214+
* @param width icon width in pixels
215+
* @param height icon height in pixels
216+
* @param pixels icon image data
217+
* @return this
218+
*/
219+
@NotNull @Contract("_, _, _ -> this")
220+
public abstract Window setIconPixels(int width, int height, byte[] pixels);
221+
207222
/**
208223
* <p>Completely removes the titlebar from the window including buttons and title.</p>
209224
*

windows/cc/WindowWin32.cc

Lines changed: 91 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -122,11 +122,81 @@ void jwm::WindowWin32::setTitlebarVisible(bool isVisible) {
122122
SWP_NOZORDER | SWP_NOACTIVATE | SWP_FRAMECHANGED);
123123
}
124124

125+
void jwm::WindowWin32::_setIconsInternal(HICON hSmall, HICON hBig) {
126+
// Apply small icon (title bar / taskbar)
127+
if (hSmall) {
128+
if (_hIconSmall)
129+
DestroyIcon(_hIconSmall);
130+
_hIconSmall = hSmall;
131+
SendMessage(_hWnd, WM_SETICON, ICON_SMALL, (LPARAM)hSmall);
132+
} else {
133+
JWM_VERBOSE("Failed to create small icon");
134+
}
135+
// Apply big icon (Alt-Tab / task switcher)
136+
if (hBig) {
137+
if (_hIconBig)
138+
DestroyIcon(_hIconBig);
139+
_hIconBig = hBig;
140+
SendMessage(_hWnd, WM_SETICON, ICON_BIG, (LPARAM)hBig);
141+
} else {
142+
JWM_VERBOSE("Failed to create big icon");
143+
}
144+
}
145+
125146
void jwm::WindowWin32::setIcon(const std::wstring& iconPath) {
126147
JWM_VERBOSE("Set window icon '" << iconPath << "'");
127-
// width / height of 0 along with LR_DEFAULTSIZE tells windows to load the default icon size.
128-
HICON hicon = (HICON)LoadImage(NULL, iconPath.c_str(), IMAGE_ICON, 0, 0, LR_LOADFROMFILE | LR_DEFAULTSIZE);
129-
SendMessage(_hWnd, WM_SETICON, ICON_SMALL, (LPARAM)hicon);
148+
int cxS = GetSystemMetrics(SM_CXSMICON), cyS = GetSystemMetrics(SM_CYSMICON);
149+
int cxB = GetSystemMetrics(SM_CXICON), cyB = GetSystemMetrics(SM_CYICON);
150+
HICON hSmall = (HICON)LoadImage(NULL, iconPath.c_str(), IMAGE_ICON, cxS, cyS, LR_LOADFROMFILE);
151+
HICON hBig = (HICON)LoadImage(NULL, iconPath.c_str(), IMAGE_ICON, cxB, cyB, LR_LOADFROMFILE);
152+
_setIconsInternal(hSmall, hBig);
153+
}
154+
155+
HICON jwm::WindowWin32::_createIconFromPixels(int width, int height, const unsigned char* argb) {
156+
// The incoming bytes are in B, G, R, A order per pixel (matching the Linux
157+
// setIconData layout), which is exactly a Windows 32bpp BGRA DIB, so we can
158+
// copy them straight into a top-down DIB section.
159+
BITMAPV5HEADER bi = {};
160+
bi.bV5Size = sizeof(BITMAPV5HEADER);
161+
bi.bV5Width = width;
162+
bi.bV5Height = -height; // negative -> top-down
163+
bi.bV5Planes = 1;
164+
bi.bV5BitCount = 32;
165+
bi.bV5Compression = BI_BITFIELDS;
166+
bi.bV5RedMask = 0x00FF0000;
167+
bi.bV5GreenMask = 0x0000FF00;
168+
bi.bV5BlueMask = 0x000000FF;
169+
bi.bV5AlphaMask = 0xFF000000;
170+
171+
HDC hdc = GetDC(NULL);
172+
void* bits = nullptr;
173+
HBITMAP hbmColor = CreateDIBSection(hdc, reinterpret_cast<BITMAPINFO*>(&bi), DIB_RGB_COLORS, &bits, NULL, 0);
174+
ReleaseDC(NULL, hdc);
175+
if (!hbmColor || !bits)
176+
return nullptr;
177+
memcpy(bits, argb, static_cast<size_t>(width) * height * 4);
178+
179+
// Monochrome mask is required but unused for 32bpp icons (alpha is honored).
180+
HBITMAP hbmMask = CreateBitmap(width, height, 1, 1, NULL);
181+
182+
ICONINFO ii = {};
183+
ii.fIcon = TRUE;
184+
ii.hbmColor = hbmColor;
185+
ii.hbmMask = hbmMask;
186+
HICON hIcon = CreateIconIndirect(&ii);
187+
188+
DeleteObject(hbmColor);
189+
DeleteObject(hbmMask);
190+
return hIcon;
191+
}
192+
193+
void jwm::WindowWin32::setIconPixels(int width, int height, const unsigned char* argb) {
194+
JWM_VERBOSE("Set window icon from raw image data " << width << "x" << height);
195+
// A single source image is used for both icon slots; Windows scales it to
196+
// the small (title bar / taskbar) and big (Alt-Tab) metrics as needed.
197+
HICON hSmall = _createIconFromPixels(width, height, argb);
198+
HICON hBig = _createIconFromPixels(width, height, argb);
199+
_setIconsInternal(hSmall, hBig);
130200
}
131201

132202
void jwm::WindowWin32::setOpacity(float opacity) {
@@ -896,6 +966,16 @@ void jwm::WindowWin32::_destroyInternal() {
896966
_hMouseCursor = nullptr;
897967
}
898968

969+
if (_hIconSmall) {
970+
DestroyIcon(_hIconSmall);
971+
_hIconSmall = nullptr;
972+
}
973+
974+
if (_hIconBig) {
975+
DestroyIcon(_hIconBig);
976+
_hIconBig = nullptr;
977+
}
978+
899979
if (_hWnd) {
900980
DestroyWindow(_hWnd);
901981
_hWnd = nullptr;
@@ -1103,6 +1183,14 @@ extern "C" JNIEXPORT void JNICALL Java_io_github_humbleui_jwm_WindowWin32__1nSet
11031183
env->ReleaseStringChars(iconPath, iconPathStr);
11041184
}
11051185

1186+
extern "C" JNIEXPORT void JNICALL Java_io_github_humbleui_jwm_WindowWin32__1nSetIconPixels
1187+
(JNIEnv* env, jobject obj, jint width, jint height, jbyteArray pixelsArr) {
1188+
jwm::WindowWin32* instance = reinterpret_cast<jwm::WindowWin32*>(jwm::classes::Native::fromJava(env, obj));
1189+
jbyte* pixels = env->GetByteArrayElements(pixelsArr, nullptr);
1190+
instance->setIconPixels(width, height, reinterpret_cast<const unsigned char*>(pixels));
1191+
env->ReleaseByteArrayElements(pixelsArr, pixels, 0);
1192+
}
1193+
11061194
extern "C" JNIEXPORT void JNICALL Java_io_github_humbleui_jwm_WindowWin32__1nSetVisible
11071195
(JNIEnv* env, jobject obj, jboolean isVisible) {
11081196
jwm::WindowWin32* instance = reinterpret_cast<jwm::WindowWin32*>(jwm::classes::Native::fromJava(env, obj));

0 commit comments

Comments
 (0)