feat(app): wire the app icon into the GUI, waybar, and SDL window

screencast_icon/ holds the single source icon set; meson install ships
the 256 px PNG into the hicolor theme for the GTK panel (themed icon
name) and the waybar CSS, and a custom target embeds it as
icon_png_data.h so the SDL receiver window sets it via SDL_SetWindowIcon
with an optional SDL3_image dependency (built-in find_library, since no
pkg-config file ships with it).
This commit is contained in:
2026-09-10 11:30:39 +02:00
parent 9ff8e94679
commit 69a1cdac1d
21 changed files with 155 additions and 11 deletions
+17
View File
@@ -65,6 +65,23 @@ loopback; current phase is Phase 7.
before the fix). Cross-compiling on the dev machine was considered and
dropped — the on-Pi build works and the toolchain/container effort was
not needed.
- **App icons wired up** (`screencast_icon/`): single 256 px PNG used
everywhere. `meson install` ships it to
`share/icons/hicolor/256x256/apps/screencast.png`; the GTK panel sets it
via `set_icon_name("screencast")` (GTK4 removed the pixel-buffer window
icon — themed names only; `gtk_icon_theme_has_icon` verified OK); the
SDL receiver window embeds the PNG at build time (`icon_png_data.h` via
`scripts/icon_to_header.py` custom_target) and `SDL_SetWindowIcon` with
an optional SDL3_image dependency (no pkg-config ships with it →
`meson.get_compiler('cpp').find_library('SDL3_image', required: false)`
+ disabler; `-DSC_HAS_WINDOW_ICON` gate; Pi builds without it still
work). The waybar widget now shows the icon as `background-image` in
`~/.config/waybar/style.css` (▶/⏸ text hidden; `.idle` dimmed,
`.streaming` green tint) instead of text glyphs; backup
`style.css.bak-20260909-*`. Lesson: icons appear in compositor
taskbars/switchers, not title bars (GTK4 CSD and Hyprland decorations
don't draw them), so validate via theme lookup, not title-bar
screenshots. `sudo pacman -S sdl3_image` done (extra/sdl3_image 3.4.6).
- **GTK panel + waybar widget**: `screencast-gui` (gtkmm-4.0, behind
`-Dgui=true`; app internals now live in the `sc_app_core` static lib so
CLI and GUI share pipelines/session/state). `screencast waybar [--toggle]`
+7 -4
View File
@@ -76,10 +76,13 @@ screencast waybar # one JSON line for a waybar custom module
screencast waybar --toggle
```
The waybar module shows `▶` while streaming (tooltip: receiver, bitrate,
elapsed) and `⏸` while idle; left-click toggles streaming to the last
receiver, right-click opens the panel. All front-ends agree on state because
the sender publishes it to `$XDG_RUNTIME_DIR/screencast/sender.json`.
The waybar module shows the app icon (dimmed while idle, highlighted while
streaming; tooltip: receiver, bitrate, elapsed); left-click toggles
streaming to the last receiver, right-click opens the panel. All front-ends
agree on state because the sender publishes it to
`$XDG_RUNTIME_DIR/screencast/sender.json`. The icon (from `screencast_icon/`)
is installed into the hicolor theme by `meson install`, used by the GTK
panel and the waybar CSS, and embedded in the SDL receiver window.
## Under the hood: resilience
+18 -5
View File
@@ -146,11 +146,24 @@ screencast waybar --toggle # stop a running sender / restart the last
```
The waybar module (installed to this machine's `~/.config/waybar/` — see
`custom/screencast` there): `▶` while streaming (tooltip: receiver, bitrate,
elapsed), `⏸` while idle; left-click toggles streaming to the last receiver
via the state file, right-click opens the panel. The sender publishes its
state to `$XDG_RUNTIME_DIR/screencast/sender.json`, so every front-end —
CLI, GUI, widget — agrees on what is running.
`custom/screencast` there): the app icon as the module face (dimmed while
idle, green tint while streaming; the `▶`/`⏸` text from the JSON is hidden
in the CSS), tooltip with receiver, bitrate, elapsed; left-click toggles
streaming to the last receiver via the state file, right-click opens the
panel. The sender publishes its state to `$XDG_RUNTIME_DIR/screencast/sender.json`,
so every front-end — CLI, GUI, widget — agrees on what is running.
**Icons**: `screencast_icon/screencast_256.png` is the single app icon.
`meson install` puts it in `share/icons/hicolor/256x256/apps/screencast.png`;
the GTK panel uses it by themed name (`set_icon_name` — GTK4 removed the
pixel-buffer window icon, so themed icons only), and the waybar CSS
references the installed path. The SDL receiver window embeds the PNG at
build time (`icon_png_data.h` generated by `scripts/icon_to_header.py`) and
sets it via `SDL_SetWindowIcon` when SDL3_image is available (optional
dependency; no pkg-config ships with it, so meson finds the library
directly). Icons surface in compositor taskbars/switchers, not in title
bars (neither GTK4's CSD nor Hyprland's decorations draw them).
Restart waybar after changing `style.css` to pick up icon changes.
## Under the hood: resilience (Phase 7)
+6
View File
@@ -26,6 +26,12 @@ if enable_tests
subdir('tests')
endif
# Install the application icon into the hicolor theme: the waybar widget CSS
# and any future .desktop file reference it.
install_data('screencast_icon/screencast_256.png',
install_dir : get_option('datadir') / 'icons/hicolor/256x256/apps',
rename : 'screencast.png')
# Summary for the user
summary({
'tests': enable_tests,
Binary file not shown.

After

Width:  |  Height:  |  Size: 4.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 398 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 84 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.8 KiB

+39
View File
@@ -0,0 +1,39 @@
#!/usr/bin/env python3
"""Generate a C++ header that embeds an image file as a byte array.
Usage: icon_to_header.py INPUT_IMAGE OUTPUT_HEADER
"""
import sys
def main() -> int:
if len(sys.argv) != 3:
print(f"usage: {sys.argv[0]} INPUT_IMAGE OUTPUT_HEADER", file=sys.stderr)
return 2
with open(sys.argv[1], "rb") as src:
data = src.read()
words = ", ".join(f"0x{byte:02x}" for byte in data)
header = f"""// GENERATED FILE - do not edit by hand.
// Produced by scripts/icon_to_header.py from screencast_icon/screencast_256.png.
#pragma once
#include <array>
#include <cstdint>
namespace sc {{
inline constexpr std::array<std::uint8_t, {len(data)}> app_icon_png = {{
{words}
}};
}} // namespace sc
"""
with open(sys.argv[2], "w") as dst:
dst.write(header)
return 0
if __name__ == "__main__":
sys.exit(main())
+3
View File
@@ -59,6 +59,9 @@ class SenderWindow : public Gtk::ApplicationWindow {
SenderWindow() {
set_title("screencast");
set_default_size(440, 440);
// GTK4 only supports themed icons: the PNG is installed into the
// hicolor theme (see meson.build) and picked up by name.
set_icon_name("screencast");
auto* box = Gtk::make_managed<Gtk::Box>(Gtk::Orientation::VERTICAL, 8);
set_child(*box);
+10
View File
@@ -1,6 +1,16 @@
# Core public headers / include dependency.
sc_core_inc = include_directories('../include')
# Embed the application icon so the GUI window and the SDL window icon work
# from the build tree, after install, and on headless targets without any
# runtime path lookup.
icon_to_header = find_program('../scripts/icon_to_header.py')
icon_png_data = custom_target('icon_png_data',
input : files('../screencast_icon/screencast_256.png'),
output : 'icon_png_data.h',
command : [icon_to_header, '@INPUT@', '@OUTPUT@'])
sc_icon_dep = declare_dependency(sources : [icon_png_data])
# The sender needs the PipeWire / xdg-desktop-portal capture backend;
# receiver-only builds skip it entirely.
build_sender = get_option('sender')
+18 -2
View File
@@ -2,14 +2,30 @@
dep_sdl3 = dependency('sdl3')
# SDL3_image (window icon) is optional: no pkg-config file ships with it,
# so find the library directly. Builds without it (e.g. the Pi receiver)
# simply skip the window icon.
cc = meson.get_compiler('cpp')
dep_sdl3_image = cc.find_library('SDL3_image', required: false)
if not dep_sdl3_image.found()
dep_sdl3_image = disabler()
endif
sc_render_args = []
if dep_sdl3_image.found() and cc.has_header('SDL3_image/SDL_image.h', dependencies : dep_sdl3_image)
sc_render_args += ['-DSC_HAS_WINDOW_ICON=1']
endif
sc_render_sources = files('sdl_renderer.cpp')
sc_render = static_library('sc_render',
sc_render_sources,
include_directories : sc_core_inc,
dependencies : [dep_sdl3])
cpp_args : sc_render_args,
dependencies : [dep_sdl3, dep_sdl3_image, sc_icon_dep])
sc_render_dep = declare_dependency(
link_with : sc_render,
include_directories : sc_core_inc,
dependencies : [dep_sdl3])
compile_args : sc_render_args,
dependencies : [dep_sdl3, dep_sdl3_image])
+37
View File
@@ -4,6 +4,16 @@
#include <string>
#ifdef SC_HAS_WINDOW_ICON
#include <SDL3_image/SDL_image.h>
#include <cstdio>
#include "icon_png_data.h"
#endif
namespace sc {
namespace {
@@ -52,6 +62,10 @@ class SdlRenderer final : public Renderer {
(void)SDL_HideCursor();
}
#ifdef SC_HAS_WINDOW_ICON
set_window_icon();
#endif
renderer_ = SDL_CreateRenderer(window_, nullptr);
if (renderer_ == nullptr) {
last_error_ = std::string{"SDL_CreateRenderer failed: "} + SDL_GetError();
@@ -155,6 +169,29 @@ class SdlRenderer final : public Renderer {
}
private:
#ifdef SC_HAS_WINDOW_ICON
// The icon is embedded at build time (see icon_png_data.h), so it works
// without any runtime file lookup. Harmless under KMSDRM, where there is
// no window manager to display it.
void set_window_icon() {
auto* stream = SDL_IOFromConstMem(sc::app_icon_png.data(), sc::app_icon_png.size());
if (stream == nullptr) {
std::fprintf(stderr, "screencast: failed to create icon stream: %s\n", SDL_GetError());
return;
}
// closeio=true: SDL_image consumes the stream, success or failure.
SDL_Surface* icon_surface = IMG_Load_IO(stream, true);
if (icon_surface == nullptr) {
std::fprintf(stderr, "screencast: failed to load window icon: %s\n", SDL_GetError());
return;
}
if (SDL_SetWindowIcon(window_, icon_surface) != 0) {
std::fprintf(stderr, "screencast: failed to set window icon: %s\n", SDL_GetError());
}
SDL_DestroySurface(icon_surface);
}
#endif
bool recreate_texture(int width, int height) {
if (texture_ != nullptr) {
SDL_DestroyTexture(texture_);