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).
@@ -65,6 +65,23 @@ loopback; current phase is Phase 7.
|
|||||||
before the fix). Cross-compiling on the dev machine was considered and
|
before the fix). Cross-compiling on the dev machine was considered and
|
||||||
dropped — the on-Pi build works and the toolchain/container effort was
|
dropped — the on-Pi build works and the toolchain/container effort was
|
||||||
not needed.
|
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
|
- **GTK panel + waybar widget**: `screencast-gui` (gtkmm-4.0, behind
|
||||||
`-Dgui=true`; app internals now live in the `sc_app_core` static lib so
|
`-Dgui=true`; app internals now live in the `sc_app_core` static lib so
|
||||||
CLI and GUI share pipelines/session/state). `screencast waybar [--toggle]`
|
CLI and GUI share pipelines/session/state). `screencast waybar [--toggle]`
|
||||||
|
|||||||
@@ -76,10 +76,13 @@ screencast waybar # one JSON line for a waybar custom module
|
|||||||
screencast waybar --toggle
|
screencast waybar --toggle
|
||||||
```
|
```
|
||||||
|
|
||||||
The waybar module shows `▶` while streaming (tooltip: receiver, bitrate,
|
The waybar module shows the app icon (dimmed while idle, highlighted while
|
||||||
elapsed) and `⏸` while idle; left-click toggles streaming to the last
|
streaming; tooltip: receiver, bitrate, elapsed); left-click toggles
|
||||||
receiver, right-click opens the panel. All front-ends agree on state because
|
streaming to the last receiver, right-click opens the panel. All front-ends
|
||||||
the sender publishes it to `$XDG_RUNTIME_DIR/screencast/sender.json`.
|
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
|
## Under the hood: resilience
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
The waybar module (installed to this machine's `~/.config/waybar/` — see
|
||||||
`custom/screencast` there): `▶` while streaming (tooltip: receiver, bitrate,
|
`custom/screencast` there): the app icon as the module face (dimmed while
|
||||||
elapsed), `⏸` while idle; left-click toggles streaming to the last receiver
|
idle, green tint while streaming; the `▶`/`⏸` text from the JSON is hidden
|
||||||
via the state file, right-click opens the panel. The sender publishes its
|
in the CSS), tooltip with receiver, bitrate, elapsed; left-click toggles
|
||||||
state to `$XDG_RUNTIME_DIR/screencast/sender.json`, so every front-end —
|
streaming to the last receiver via the state file, right-click opens the
|
||||||
CLI, GUI, widget — agrees on what is running.
|
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)
|
## Under the hood: resilience (Phase 7)
|
||||||
|
|
||||||
|
|||||||
@@ -26,6 +26,12 @@ if enable_tests
|
|||||||
subdir('tests')
|
subdir('tests')
|
||||||
endif
|
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 for the user
|
||||||
summary({
|
summary({
|
||||||
'tests': enable_tests,
|
'tests': enable_tests,
|
||||||
|
|||||||
|
After Width: | Height: | Size: 4.4 KiB |
|
After Width: | Height: | Size: 2.7 KiB |
|
After Width: | Height: | Size: 6.2 KiB |
|
After Width: | Height: | Size: 10 KiB |
|
After Width: | Height: | Size: 16 KiB |
|
After Width: | Height: | Size: 398 KiB |
|
After Width: | Height: | Size: 9.1 KiB |
|
After Width: | Height: | Size: 25 KiB |
|
After Width: | Height: | Size: 1.7 KiB |
|
After Width: | Height: | Size: 2.7 KiB |
|
After Width: | Height: | Size: 84 KiB |
|
After Width: | Height: | Size: 3.8 KiB |
@@ -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())
|
||||||
@@ -59,6 +59,9 @@ class SenderWindow : public Gtk::ApplicationWindow {
|
|||||||
SenderWindow() {
|
SenderWindow() {
|
||||||
set_title("screencast");
|
set_title("screencast");
|
||||||
set_default_size(440, 440);
|
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);
|
auto* box = Gtk::make_managed<Gtk::Box>(Gtk::Orientation::VERTICAL, 8);
|
||||||
set_child(*box);
|
set_child(*box);
|
||||||
|
|||||||
@@ -1,6 +1,16 @@
|
|||||||
# Core public headers / include dependency.
|
# Core public headers / include dependency.
|
||||||
sc_core_inc = include_directories('../include')
|
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;
|
# The sender needs the PipeWire / xdg-desktop-portal capture backend;
|
||||||
# receiver-only builds skip it entirely.
|
# receiver-only builds skip it entirely.
|
||||||
build_sender = get_option('sender')
|
build_sender = get_option('sender')
|
||||||
|
|||||||
@@ -2,14 +2,30 @@
|
|||||||
|
|
||||||
dep_sdl3 = dependency('sdl3')
|
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_sources = files('sdl_renderer.cpp')
|
||||||
|
|
||||||
sc_render = static_library('sc_render',
|
sc_render = static_library('sc_render',
|
||||||
sc_render_sources,
|
sc_render_sources,
|
||||||
include_directories : sc_core_inc,
|
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(
|
sc_render_dep = declare_dependency(
|
||||||
link_with : sc_render,
|
link_with : sc_render,
|
||||||
include_directories : sc_core_inc,
|
include_directories : sc_core_inc,
|
||||||
dependencies : [dep_sdl3])
|
compile_args : sc_render_args,
|
||||||
|
dependencies : [dep_sdl3, dep_sdl3_image])
|
||||||
@@ -4,6 +4,16 @@
|
|||||||
|
|
||||||
#include <string>
|
#include <string>
|
||||||
|
|
||||||
|
#ifdef SC_HAS_WINDOW_ICON
|
||||||
|
|
||||||
|
#include <SDL3_image/SDL_image.h>
|
||||||
|
|
||||||
|
#include <cstdio>
|
||||||
|
|
||||||
|
#include "icon_png_data.h"
|
||||||
|
|
||||||
|
#endif
|
||||||
|
|
||||||
namespace sc {
|
namespace sc {
|
||||||
namespace {
|
namespace {
|
||||||
|
|
||||||
@@ -52,6 +62,10 @@ class SdlRenderer final : public Renderer {
|
|||||||
(void)SDL_HideCursor();
|
(void)SDL_HideCursor();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#ifdef SC_HAS_WINDOW_ICON
|
||||||
|
set_window_icon();
|
||||||
|
#endif
|
||||||
|
|
||||||
renderer_ = SDL_CreateRenderer(window_, nullptr);
|
renderer_ = SDL_CreateRenderer(window_, nullptr);
|
||||||
if (renderer_ == nullptr) {
|
if (renderer_ == nullptr) {
|
||||||
last_error_ = std::string{"SDL_CreateRenderer failed: "} + SDL_GetError();
|
last_error_ = std::string{"SDL_CreateRenderer failed: "} + SDL_GetError();
|
||||||
@@ -155,6 +169,29 @@ class SdlRenderer final : public Renderer {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private:
|
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) {
|
bool recreate_texture(int width, int height) {
|
||||||
if (texture_ != nullptr) {
|
if (texture_ != nullptr) {
|
||||||
SDL_DestroyTexture(texture_);
|
SDL_DestroyTexture(texture_);
|
||||||
|
|||||||