Make the app icon work on macOS, Windows, and Linux.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
pepaskopec-blip 2026-09-15 14:44:45 +02:00
commit 3ea022dd4f
16 changed files with 236 additions and 17 deletions

View file

@ -46,7 +46,8 @@ endif()
# so MSVC needs mainCRTStartup as the entry point. # so MSVC needs mainCRTStartup as the entry point.
file(GLOB MATURITA_SOURCES CONFIGURE_DEPENDS "${CMAKE_CURRENT_SOURCE_DIR}/*.c") file(GLOB MATURITA_SOURCES CONFIGURE_DEPENDS "${CMAKE_CURRENT_SOURCE_DIR}/*.c")
if(WIN32) if(WIN32)
add_executable(maturita WIN32 ${MATURITA_SOURCES}) add_executable(maturita WIN32 ${MATURITA_SOURCES}
"${CMAKE_CURRENT_SOURCE_DIR}/maturita.rc")
else() else()
add_executable(maturita ${MATURITA_SOURCES}) add_executable(maturita ${MATURITA_SOURCES})
endif() endif()
@ -66,8 +67,8 @@ else()
endif() endif()
endif() endif()
# The GTK stylesheet and window icon are loaded at runtime from the working # The GTK stylesheet, window icon and Freedesktop share/ tree are loaded at
# directory (or next to the binary), so keep copies beside the built binary. # runtime from the working directory (or next to the binary).
add_custom_command(TARGET maturita POST_BUILD add_custom_command(TARGET maturita POST_BUILD
COMMAND ${CMAKE_COMMAND} -E copy_if_different COMMAND ${CMAKE_COMMAND} -E copy_if_different
${CMAKE_CURRENT_SOURCE_DIR}/style.css ${CMAKE_CURRENT_SOURCE_DIR}/style.css
@ -81,4 +82,7 @@ add_custom_command(TARGET maturita POST_BUILD
${CMAKE_CURRENT_SOURCE_DIR}/assets/icons/hicolor/512x512/apps ${CMAKE_CURRENT_SOURCE_DIR}/assets/icons/hicolor/512x512/apps
COMMAND ${CMAKE_COMMAND} -E copy_if_different COMMAND ${CMAKE_COMMAND} -E copy_if_different
${CMAKE_CURRENT_SOURCE_DIR}/assets/app-icon.png ${CMAKE_CURRENT_SOURCE_DIR}/assets/app-icon.png
${CMAKE_CURRENT_SOURCE_DIR}/assets/icons/hicolor/512x512/apps/maturita.png) ${CMAKE_CURRENT_SOURCE_DIR}/assets/icons/hicolor/512x512/apps/maturita.png
COMMAND ${CMAKE_COMMAND} -E copy_directory
${CMAKE_CURRENT_SOURCE_DIR}/share
$<TARGET_FILE_DIR:maturita>/share)

View file

@ -17,12 +17,15 @@ GTK_LIBS := $(shell $(PKG_CONFIG) --libs gtk4)
CFLAGS = -Wall -Wextra -Wno-deprecated-declarations $(GTK_CFLAGS) CFLAGS = -Wall -Wextra -Wno-deprecated-declarations $(GTK_CFLAGS)
LIBS = $(GTK_LIBS) -lm LIBS = $(GTK_LIBS) -lm
# Windows (MSYS2 / MinGW): .exe suffix and no console window for the GUI. # Windows (MSYS2 / MinGW): .exe suffix, no console window, embed .ico.
ifeq ($(OS),Windows_NT) ifeq ($(OS),Windows_NT)
TARGET = maturita.exe TARGET = maturita.exe
LIBS += -mwindows LIBS += -mwindows
WINDRES ?= windres
RC_OBJ = maturita_rc.o
else else
TARGET = maturita TARGET = maturita
RC_OBJ =
# macOS Dock icon is set via AppKit (see macos_dock.c). # macOS Dock icon is set via AppKit (see macos_dock.c).
ifeq ($(shell uname -s 2>/dev/null),Darwin) ifeq ($(shell uname -s 2>/dev/null),Darwin)
LIBS += -framework AppKit -framework Foundation LIBS += -framework AppKit -framework Foundation
@ -34,6 +37,7 @@ OBJ = $(SRC:.c=.o)
ICON_SRC = assets/app-icon.png ICON_SRC = assets/app-icon.png
ICON_DST = assets/icons/hicolor/512x512/apps/maturita.png ICON_DST = assets/icons/hicolor/512x512/apps/maturita.png
ICON_ICO = assets/app-icon.ico
all: $(TARGET) all: $(TARGET)
@ -41,11 +45,19 @@ $(ICON_DST): $(ICON_SRC)
mkdir -p $(dir $@) mkdir -p $(dir $@)
cp "$(ICON_SRC)" "$@" cp "$(ICON_SRC)" "$@"
ifeq ($(OS),Windows_NT)
$(RC_OBJ): maturita.rc $(ICON_ICO)
$(WINDRES) maturita.rc -o $(RC_OBJ)
$(TARGET): $(SRC) $(ICON_DST) $(RC_OBJ)
$(CC) $(CFLAGS) -o $(TARGET) $(SRC) $(RC_OBJ) $(LIBS)
else
$(TARGET): $(SRC) $(ICON_DST) $(TARGET): $(SRC) $(ICON_DST)
$(CC) $(CFLAGS) -o $(TARGET) $(SRC) $(LIBS) $(CC) $(CFLAGS) -o $(TARGET) $(SRC) $(LIBS)
endif
clean: clean:
rm -f $(TARGET) maturita.exe $(OBJ) rm -f $(TARGET) maturita.exe $(OBJ) $(RC_OBJ) maturita_rc.o
run: $(TARGET) run: $(TARGET)
./$(TARGET) ./$(TARGET)
@ -61,6 +73,7 @@ bundle: $(TARGET)
@cp style.css dist/ @cp style.css dist/
@mkdir -p dist/icons/hicolor/512x512/apps @mkdir -p dist/icons/hicolor/512x512/apps
@cp "$(ICON_DST)" dist/icons/hicolor/512x512/apps/ @cp "$(ICON_DST)" dist/icons/hicolor/512x512/apps/
@cp -R share dist/
@ldd $(TARGET) | grep -Ei '/(ucrt64|mingw64)/bin/' | awk '{print $$3}' \ @ldd $(TARGET) | grep -Ei '/(ucrt64|mingw64)/bin/' | awk '{print $$3}' \
| xargs -r -I{} cp -f {} dist/ | xargs -r -I{} cp -f {} dist/
@echo "Bundled into dist/ - run dist/$(TARGET)" @echo "Bundled into dist/ - run dist/$(TARGET)"

BIN
assets/app-icon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 75 KiB

View file

@ -1,5 +1,5 @@
@echo off @echo off
setlocal setlocal EnableExtensions
REM --------------------------------------------------------------------------- REM ---------------------------------------------------------------------------
REM Native Windows build with MSVC. No MSYS2 or MinGW shell required. REM Native Windows build with MSVC. No MSYS2 or MinGW shell required.
@ -67,10 +67,22 @@ if not defined GTK_CFLAGS (
exit /b 1 exit /b 1
) )
REM --- 4. Compile ----------------------------------------------------------- if not exist "assets\app-icon.ico" (
echo [error] assets\app-icon.ico is missing.
echo Generate it with: python scripts\gen-icons.py
exit /b 1
)
REM --- 4. Compile resources + sources ---------------------------------------
echo Building maturita.exe with MSVC... echo Building maturita.exe with MSVC...
rc /nologo /fo maturita.res maturita.rc
if errorlevel 1 (
echo [error] Resource compile failed.
exit /b 1
)
cl /nologo /O2 /MD /W3 /std:c11 /D_CRT_SECURE_NO_WARNINGS ^ cl /nologo /O2 /MD /W3 /std:c11 /D_CRT_SECURE_NO_WARNINGS ^
%GTK_CFLAGS% maturita.c ^ %GTK_CFLAGS% *.c maturita.res ^
/Fe:maturita.exe ^ /Fe:maturita.exe ^
/link /SUBSYSTEM:WINDOWS /ENTRY:mainCRTStartup %GTK_LIBS% /link /SUBSYSTEM:WINDOWS /ENTRY:mainCRTStartup %GTK_LIBS%
@ -79,7 +91,10 @@ if errorlevel 1 (
exit /b 1 exit /b 1
) )
if not exist icons\hicolor\512x512\apps mkdir icons\hicolor\512x512\apps
copy /Y assets\app-icon.png icons\hicolor\512x512\apps\maturita.png >nul
echo. echo.
echo Built maturita.exe echo Built maturita.exe ^(icon embedded from assets\app-icon.ico^)
echo Run it from this terminal with: maturita.exe echo Run it from this terminal with: maturita.exe
echo Or use run-windows.bat / double-click maturita.exe. echo Or use run-windows.bat / double-click maturita.exe.

97
main.c
View file

@ -189,6 +189,7 @@ static const char *find_app_icon_file(void) {
static const char *paths[] = { static const char *paths[] = {
ICON_FILE, ICON_FILE,
ICON_THEME_DIR "/hicolor/512x512/apps/" ICON_NAME ".png", ICON_THEME_DIR "/hicolor/512x512/apps/" ICON_NAME ".png",
SHARE_DIR "/icons/hicolor/512x512/apps/" ICON_NAME ".png",
ICON_FILE_ALT, ICON_FILE_ALT,
NULL, NULL,
}; };
@ -201,12 +202,84 @@ static const char *find_app_icon_file(void) {
return NULL; return NULL;
} }
/* GTK4 has no gtk_window_set_icon_from_file(); register a local hicolor /* Kept alive for gdk_toplevel_set_icon_list() which does not take ownership. */
* theme dir and set the window icon by name instead. On macOS the Dock static GList *app_icon_textures;
* tile ignores that, so also push the PNG through AppKit. */
static void clear_app_icon_textures(void) {
g_list_free_full(app_icon_textures, g_object_unref);
app_icon_textures = NULL;
}
/* X11 / some compositors use the toplevel icon list for the taskbar tile. */
static void apply_toplevel_icon(GtkWindow *window) {
GdkSurface *surface;
const char *path;
GError *err = NULL;
GdkTexture *tex;
surface = gtk_native_get_surface(GTK_NATIVE(window));
if (!surface || !GDK_IS_TOPLEVEL(surface))
return;
path = find_app_icon_file();
if (!path)
return;
tex = gdk_texture_new_from_filename(path, &err);
if (!tex) {
g_clear_error(&err);
return;
}
clear_app_icon_textures();
app_icon_textures = g_list_append(NULL, tex);
gdk_toplevel_set_icon_list(GDK_TOPLEVEL(surface), app_icon_textures);
}
static void on_window_realize_icon(GtkWidget *widget, gpointer data) {
(void)data;
apply_toplevel_icon(GTK_WINDOW(widget));
}
/* Prepend ./share to XDG_DATA_DIRS so Wayland can find our .desktop file and
* hicolor icons without a system install (matches APP_ID). */
static void setup_portable_share_dir(void) {
char *abs;
const char *old;
char *neu;
if (!g_file_test(SHARE_DIR "/applications", G_FILE_TEST_IS_DIR) &&
!g_file_test(SHARE_DIR "/icons", G_FILE_TEST_IS_DIR))
return;
abs = g_canonicalize_filename(SHARE_DIR, NULL);
if (!abs)
return;
old = g_getenv("XDG_DATA_DIRS");
if (old && old[0])
neu = g_strconcat(abs, G_SEARCHPATH_SEPARATOR_S, old, NULL);
else
neu = g_strconcat(abs,
G_SEARCHPATH_SEPARATOR_S "/usr/local/share"
G_SEARCHPATH_SEPARATOR_S "/usr/share",
NULL);
g_setenv("XDG_DATA_DIRS", neu, TRUE);
g_free(neu);
g_free(abs);
}
/* Register icon theme paths + window icon-name (Linux), AppKit Dock (macOS),
* and toplevel textures (X11 / fallbacks). Windows uses the .ico embedded
* via maturita.rc at link time. */
static void setup_window_icon(GtkWindow *window) { static void setup_window_icon(GtkWindow *window) {
GtkIconTheme *theme; GtkIconTheme *theme;
static const char *dirs[] = { ICON_THEME_DIR, ICON_THEME_DIR_ALT, NULL }; static const char *dirs[] = {
ICON_THEME_DIR,
ICON_THEME_DIR_ALT,
SHARE_DIR "/icons",
NULL,
};
const char *icon_file; const char *icon_file;
int i; int i;
@ -220,16 +293,21 @@ static void setup_window_icon(GtkWindow *window) {
gtk_window_set_default_icon_name(ICON_NAME); gtk_window_set_default_icon_name(ICON_NAME);
gtk_window_set_icon_name(window, ICON_NAME); gtk_window_set_icon_name(window, ICON_NAME);
} else { } else {
g_warning("App icon '%s' not found (tried %s/ and %s/)", g_warning("App icon '%s' not found (tried %s/, %s/, %s/icons/)",
ICON_NAME, ICON_THEME_DIR, ICON_THEME_DIR_ALT); ICON_NAME, ICON_THEME_DIR, ICON_THEME_DIR_ALT, SHARE_DIR);
} }
icon_file = find_app_icon_file(); icon_file = find_app_icon_file();
if (icon_file) if (icon_file)
macos_set_dock_icon(icon_file); macos_set_dock_icon(icon_file);
#ifdef __APPLE__
else else
g_warning("Dock icon PNG not found (tried %s and %s)", g_warning("Dock icon PNG not found (tried %s and %s)",
ICON_FILE, ICON_FILE_ALT); ICON_FILE, ICON_FILE_ALT);
#endif
g_signal_connect(window, "realize",
G_CALLBACK(on_window_realize_icon), NULL);
} }
void activate(GtkApplication *app, gpointer user_data) { void activate(GtkApplication *app, gpointer user_data) {
@ -398,9 +476,14 @@ int main(int argc, char **argv) {
GtkApplication *app; GtkApplication *app;
int status; int status;
app = gtk_application_new("org.example.maturita", G_APPLICATION_DEFAULT_FLAGS); /* Must run before GtkApplication so Wayland can resolve our .desktop
* file / icons via the application id. */
setup_portable_share_dir();
app = gtk_application_new(APP_ID, G_APPLICATION_DEFAULT_FLAGS);
g_signal_connect(app, "activate", G_CALLBACK(activate), NULL); g_signal_connect(app, "activate", G_CALLBACK(activate), NULL);
status = g_application_run(G_APPLICATION(app), argc, argv); status = g_application_run(G_APPLICATION(app), argc, argv);
clear_app_icon_textures();
g_object_unref(app); g_object_unref(app);
return status; return status;

View file

@ -15,11 +15,13 @@
#define HW_SUBJ 2 /* index of "Technické vybavení" */ #define HW_SUBJ 2 /* index of "Technické vybavení" */
#define CZ_SUBJ 3 /* index of "Český jazyk a literatura" */ #define CZ_SUBJ 3 /* index of "Český jazyk a literatura" */
#define CSS_FILE "style.css" #define CSS_FILE "style.css"
#define APP_ID "org.maturita.Maturita"
#define ICON_NAME "maturita" #define ICON_NAME "maturita"
#define ICON_FILE "assets/app-icon.png" #define ICON_FILE "assets/app-icon.png"
#define ICON_THEME_DIR "assets/icons" /* cwd when run from source tree */ #define ICON_THEME_DIR "assets/icons" /* cwd when run from source tree */
#define ICON_THEME_DIR_ALT "icons" /* next to the binary after install */ #define ICON_THEME_DIR_ALT "icons" /* next to the binary after install */
#define ICON_FILE_ALT "icons/hicolor/512x512/apps/maturita.png" #define ICON_FILE_ALT "icons/hicolor/512x512/apps/maturita.png"
#define SHARE_DIR "share" /* portable Freedesktop data root */
#define PROGRESS_DIR "progress" #define PROGRESS_DIR "progress"
#define PROGRESS_U1 "progress/unit1.conf" #define PROGRESS_U1 "progress/unit1.conf"
#define PROGRESS_U2 "progress/unit2.conf" #define PROGRESS_U2 "progress/unit2.conf"

3
maturita.rc Normal file
View file

@ -0,0 +1,3 @@
/* Windows application icon embedded into maturita.exe.
* The first ICON resource becomes the Explorer / taskbar icon. */
1 ICON "assets/app-icon.ico"

85
scripts/gen-icons.py Executable file
View file

@ -0,0 +1,85 @@
#!/usr/bin/env python3
"""Regenerate assets/app-icon.ico and share/icons from assets/app-icon.png.
Uses macOS `sips` when available; otherwise requires Pillow.
Run from the repository root.
"""
from __future__ import annotations
import struct
import subprocess
import sys
import tempfile
from pathlib import Path
ROOT = Path(__file__).resolve().parent.parent
SRC = ROOT / "assets" / "app-icon.png"
ICO = ROOT / "assets" / "app-icon.ico"
ICO_SIZES = [16, 32, 48, 64, 128, 256]
SHARE_SIZES = [16, 32, 48, 64, 128, 256, 512]
def resize_png(src: Path, dest: Path, size: int) -> None:
dest.parent.mkdir(parents=True, exist_ok=True)
if subprocess.call(["which", "sips"], stdout=subprocess.DEVNULL,
stderr=subprocess.DEVNULL) == 0:
subprocess.check_call(
["sips", "-z", str(size), str(size), str(src), "--out", str(dest)],
stdout=subprocess.DEVNULL,
)
return
try:
from PIL import Image
except ImportError as exc:
raise SystemExit(
"Need macOS sips or Pillow to resize icons"
) from exc
img = Image.open(src).convert("RGBA")
img = img.resize((size, size), Image.Resampling.LANCZOS)
img.save(dest, format="PNG")
def write_ico(entries: list[tuple[int, bytes]], dest: Path) -> None:
count = len(entries)
header = struct.pack("<HHH", 0, 1, count)
offset = 6 + 16 * count
direntries = []
blobs = []
for size, data in entries:
w = 0 if size >= 256 else size
h = 0 if size >= 256 else size
direntries.append(
struct.pack("<BBBBHHII", w, h, 0, 0, 1, 32, len(data), offset)
)
blobs.append(data)
offset += len(data)
dest.write_bytes(header + b"".join(direntries) + b"".join(blobs))
def main() -> int:
if not SRC.is_file():
print(f"missing {SRC}", file=sys.stderr)
return 1
with tempfile.TemporaryDirectory(prefix="maturita-ico-") as tmp:
entries: list[tuple[int, bytes]] = []
for size in ICO_SIZES:
png = Path(tmp) / f"icon_{size}.png"
resize_png(SRC, png, size)
entries.append((size, png.read_bytes()))
write_ico(entries, ICO)
print(f"wrote {ICO.relative_to(ROOT)}")
for size in SHARE_SIZES:
dest = ROOT / "share" / "icons" / "hicolor" / f"{size}x{size}" / "apps" / "maturita.png"
resize_png(SRC, dest, size)
print(f"wrote {dest.relative_to(ROOT)}")
assets_icon = ROOT / "assets" / "icons" / "hicolor" / "512x512" / "apps" / "maturita.png"
resize_png(SRC, assets_icon, 512)
print(f"wrote {assets_icon.relative_to(ROOT)}")
return 0
if __name__ == "__main__":
raise SystemExit(main())

View file

@ -0,0 +1,14 @@
[Desktop Entry]
Type=Application
Version=1.0
Name=maturita.c
GenericName=Maturita study app
GenericName[cs]=Příprava na maturitu
Comment=Educational GTK app for maturita exam preparation
Comment[cs]=Vzdělávací GTK aplikace pro přípravu na maturitu
Exec=maturita
Icon=maturita
Terminal=false
Categories=Education;GTK;
StartupNotify=true
StartupWMClass=org.maturita.Maturita

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 47 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 149 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.5 KiB