Let the launcher find an icon for the window

The icon was already embedded and had been all along: `app.slint` sets it and
`build.rs` compiles it into the binary with `EmbedFiles`. That is not what a
Wayland compositor looks at. It ignores a client-set icon entirely, matches
the surface's `app_id` against installed `.desktop` files, and takes the icon
from the one whose basename agrees. Nothing set an app id and no desktop entry
existed, so GNOME had nothing to resolve and drew the placeholder.

Both halves are needed and neither substitutes for the other: the embedded
icon is what X11 and the window itself use, and the desktop entry is what the
overview, the dash and alt-tab use.

The app id, the `.desktop` basename and the installed icon's filename are one
string in three places — `paris.tourolle.darkroom`, the same reverse-DNS name
the Android manifest already uses, because it is one application. Change one
without the others and the icon disappears again with nothing logged, so each
file says so where the string appears.

The PKGBUILD builds from the checkout rather than a release tarball, so
`makepkg -si` installs what is being worked on. Install verified by inspection
of the built package: binary, desktop entry, and the icon under
hicolor/256x256 by the name the entry asks for.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-08-23 10:26:04 +02:00
co-authored by Claude Opus 5
parent 2a4a48c0fb
commit e25f0ad2c6
4 changed files with 97 additions and 0 deletions
+9
View File
@@ -2,3 +2,12 @@
/target-android
Cargo.lock.bak
*.log
# makepkg build products. `packaging/PKGBUILD` and the .desktop entry are
# sources and belong in the tree; everything makepkg derives from them does
# not — `pkg/` and `src/` are staging directories it recreates on every run,
# and the package itself is 33 MB of compiled output.
/packaging/pkg/
/packaging/src/
/packaging/*.pkg.tar.*
/packaging/*.log
+46
View File
@@ -0,0 +1,46 @@
# Maintainer: Duncan Tourolle <duncan@tourolle.paris>
#
# Builds from the local checkout rather than a release tarball, which is what
# makes `makepkg -si` in this directory install what you are actually working
# on. Swap `source` for a tagged tarball when there is something to release.
pkgname=darkroom
pkgver=0.3.0
pkgrel=1
pkgdesc="Non-destructive RAW photo library and editor"
arch=('x86_64')
url="https://gitea.tourolle.paris/dtourolle/DarkRoom"
license=('GPL-3.0-or-later')
# Runtime: Vulkan for wgpu, and a Secret Service implementation for the
# Nextcloud credentials (FR-NC-2) — gnome-keyring or kwallet both provide it.
depends=('vulkan-icd-loader' 'fontconfig' 'libxkbcommon')
makedepends=('cargo' 'git')
optdepends=('gnome-keyring: store Nextcloud credentials'
'kwallet: store Nextcloud credentials')
options=('!lto') # the workspace sets its own LTO in Cargo.toml
_repo="$(cd "${startdir}/.." && pwd)"
build() {
cd "${_repo}"
export RUSTUP_TOOLCHAIN=stable
export CARGO_TARGET_DIR="${_repo}/target"
cargo build --release --locked -p darkroom-desktop
}
package() {
cd "${_repo}"
install -Dm755 "target/release/darkroom-desktop" \
"${pkgdir}/usr/bin/darkroom-desktop"
install -Dm644 "packaging/paris.tourolle.darkroom.desktop" \
"${pkgdir}/usr/share/applications/paris.tourolle.darkroom.desktop"
# The icon's *name* is the contract, not its path: the desktop entry says
# `Icon=paris.tourolle.darkroom` and the compositor resolves that through
# the hicolor theme. Installed under 256x256 because that is the source's
# actual size — lying about it makes scaled-down icons look wrong.
install -Dm644 "ui/dr-ui/ui/app-icon.png" \
"${pkgdir}/usr/share/icons/hicolor/256x256/apps/paris.tourolle.darkroom.png"
install -Dm644 "README.md" "${pkgdir}/usr/share/doc/${pkgname}/README.md"
}
+18
View File
@@ -0,0 +1,18 @@
[Desktop Entry]
Type=Application
Name=DarkRoom
GenericName=Photo Editor
Comment=Non-destructive RAW photo library and editor
# The basename of this file is the contract: a Wayland compositor finds it by
# matching the surface's app_id, which `dr_ui::run` sets to the same string.
# Rename one without the other and the icon silently disappears.
Exec=darkroom-desktop %F
Icon=paris.tourolle.darkroom
Terminal=false
Categories=Graphics;Photography;RasterGraphics;2DGraphics;
Keywords=RAW;photo;photography;develop;darkroom;library;
# X11 falls back to matching the WM_CLASS instead of the app_id; winit sets it
# from the same string, so naming it here covers both display servers.
StartupWMClass=paris.tourolle.darkroom
StartupNotify=true
MimeType=image/x-canon-cr2;image/x-canon-cr3;image/x-nikon-nef;image/x-sony-arw;image/x-adobe-dng;image/x-fuji-raf;image/x-olympus-orf;image/x-panasonic-rw2;image/jpeg;image/png;image/tiff;
+24
View File
@@ -733,6 +733,30 @@ pub fn run(paths: Vec<PathBuf>) -> Result<()> {
let entries = Rc::new(RefCell::new(collect(&paths)));
log::info!("{} image(s) to browse", entries.borrow().len());
// TRACES: FR-PLAT-LIN-1
// The name the compositor knows this window by, and the reason the
// launcher shows a real icon rather than a grey square.
//
// `app.slint` sets `icon:`, and that is genuinely embedded (see
// `build.rs`) — but a Wayland compositor ignores a client-set icon
// entirely. It takes the icon from the `.desktop` file whose basename
// matches the surface's `app_id`, and nothing else. So the embedded icon
// is what X11 and the window itself use, and *this* is what GNOME's
// overview, dash and alt-tab use. Both are needed and neither substitutes
// for the other.
//
// The string must equal the installed `.desktop` file's basename exactly:
// `packaging/paris.tourolle.darkroom.desktop`. It matches the Android
// package id (`AndroidManifest.xml`) on purpose — one application, one
// reverse-DNS name on both platforms.
//
// Before the window, necessarily: it is read when the surface is created.
if let Err(e) = slint::set_xdg_app_id("paris.tourolle.darkroom") {
// Not fatal. On X11 and on Android this does nothing useful, and a
// missing app id costs an icon rather than a working application.
log::debug!("could not set the xdg app id: {e}");
}
// Before the window, and it has to be: this selects the Slint backend, and
// creating a window selects one for us. See `shared_gpu`. The device is
// shared by demosaic, the adjust pass and the compositor; without one the