Files
Ferrous-Solitaire/solitaire_app/Cargo.toml
T
funman300 3eb3a26789 feat(app): wire desktop window icon — Terminal ▌RS mark at runtime
Closes Resume-prompt Option A (the post-v0.21.0 first option).
Half-day desktop work, no cert dependency.

Three deliverables:

1. **SVG-authored icon** (`solitaire_engine/src/assets/icon_svg.rs`)
   — square Terminal mark: `#151515` background, brick-red
   `#a54242` 1 px border, brick-red ▌ cursor block centered, "RS"
   monogram in `#d0d0d0` foreground gray beneath. Same shape that
   already lives on the splash boot screen and card-back monogram,
   reused as the project's signature visual mark. Authored in a
   64-unit logical box so it scales cleanly at every rasterisation
   target.

2. **9-size PNG hierarchy** (16, 24, 32, 48, 64, 128, 256, 512, 1024
   px) regenerated by `solitaire_engine/examples/icon_generator.rs`
   into `assets/icon/icon_<size>.png`. Sizes cover Linux hicolor
   (16, 24, 32, 48, 64, 128, 256, 512), Windows .ico targets (16,
   32, 48, 256), and macOS .icns targets (16, 32, 64, 128, 256,
   512, 1024). The runtime path uses just the 256 px slot; the
   smaller sizes are pre-rendered for downstream packaging.

3. **Runtime `Window::icon` wiring** (`solitaire_app/src/lib.rs`).
   Bevy 0.18 has no `Window::icon` field — the icon is set through
   the underlying `winit::window::Window` via the `WinitWindows`
   resource. `set_window_icon` runs each Update tick, retries
   silently until `WinitWindows` is populated (typically frame 1
   or 2), decodes the embedded 256 px PNG via `tiny_skia`, builds
   a `winit::window::Icon`, and self-disables via `Local<bool>`.
   Same one-shot pattern as `apply_smart_default_window_size`.
   Desktop-only — Android draws its launcher icon from the APK
   manifest, so the system is target-gated to
   `cfg(not(target_os = "android"))`.

Dep changes (CLAUDE.md §8 user-confirmed):

- `winit = "0.30"` promoted from a transitive Bevy dep to a direct
  dep on `solitaire_app` so `winit::window::Icon` is in scope —
  bevy_winit 0.18 doesn't re-export it. Version pinned to whatever
  Bevy uses; if Bevy bumps winit, this line bumps in lockstep.
- `tiny-skia` added as a direct dep on `solitaire_app` for PNG →
  RGBA decode. Already in workspace deps for `solitaire_engine`;
  no version drift risk.
- Both new deps target-gated to non-Android only.

Test infrastructure: `solitaire_engine/tests/icon_svg_pin.rs`
hashes the rasterised RGBA bytes at all 9 sizes via FNV-1a (same
shape as `card_face_svg_pin`). Bootstrap pattern (empty EXPECTED
→ panic with hashes formatted as Rust source → paste back in)
handles future intentional builder edits cleanly.

Workspace clippy + cargo test --workspace clean. 1185 passing
(+1 from v0.21.0's 1184 baseline — the icon pin's
`rasterised_icon_bytes_match_pinned_hashes`).

Out of scope for this commit: `.icns` / `.ico` bundling for
macOS / Windows app packaging. Both are packaging-time concerns
(set via bundle manifests, not runtime calls) and would need new
deps (`ico` and `icns` crates) — separate followup if/when the
project ships as a packaged macOS / Windows app rather than just
`cargo run`.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-08 11:07:31 -07:00

85 lines
3.4 KiB
TOML

[package]
name = "solitaire_app"
version.workspace = true
license.workspace = true
edition.workspace = true
[[bin]]
name = "solitaire_app"
path = "src/main.rs"
# `cdylib` is what cargo-apk packages into `libsolitaire_app.so` for
# Android — the activity dlopens the shared object and calls into it.
# `rlib` lets the bin target above link the library normally on
# desktop. Both produce the same code; only the linkage form differs.
[lib]
name = "solitaire_app"
path = "src/lib.rs"
crate-type = ["cdylib", "rlib"]
[dependencies]
bevy = { workspace = true }
solitaire_engine = { workspace = true }
solitaire_data = { workspace = true }
# Desktop-only deps. `keyring`'s default-store init only matters on
# platforms with a real keychain backend (Linux Secret Service,
# macOS Keychain, Windows Credential Store), and its transitive
# `rpassword` uses `libc::__errno_location` — a symbol Android's
# bionic doesn't expose. `winit` is promoted from a transitive
# Bevy 0.18 → bevy_winit 0.18 → winit 0.30 dep to a direct dep so
# the `Window::icon` wiring in `set_window_icon` can construct
# `winit::window::Icon` values (bevy_winit 0.18 doesn't re-export
# `Icon`). Android draws its launcher icon from the APK manifest,
# so neither dep matters there. Target-gating keeps `cargo apk
# build` viable; the desktop call sites have their own
# `cfg(not(target_os = "android"))` guards.
[target.'cfg(not(target_os = "android"))'.dependencies]
keyring = { workspace = true }
winit = { version = "0.30", default-features = false }
# `tiny-skia` is already in the workspace deps for `solitaire_engine`;
# `solitaire_app` consumes it directly only on the desktop icon path
# (PNG → raw RGBA decode for `set_window_icon`).
tiny-skia = { workspace = true }
# --- Android packaging metadata (read by `cargo-apk`) -------------------
#
# Pinning these values inside the repo means a contributor running
# `cargo apk build -p solitaire_app --target x86_64-linux-android`
# does not need to install whatever SDK version cargo-apk happens to
# default to today. The numbers track the SDK we install in the dev
# setup script: target SDK 34 (Android 14, current Play Store target),
# min SDK 26 (Android 8, the lowest Bevy 0.18 supports cleanly with
# the wgpu / GLES path).
#
# Asset path is `../assets` so the same directory the desktop build
# already uses ships into the APK without copy-tree gymnastics.
# `apk_name` keeps the output filename predictable across machines.
[package.metadata.android]
package = "com.solitairequest.app"
apk_name = "solitaire-quest"
build_targets = ["aarch64-linux-android", "armv7-linux-androideabi", "x86_64-linux-android"]
assets = "../assets"
# No `runtime_libs` — we don't ship any precompiled .so files,
# the entire app is pure Rust + Bevy. cargo-apk would try to
# resolve `runtime_libs/<arch>/` if set, and fail on a non-existent
# arch directory under our package.
strip = "strip"
[package.metadata.android.sdk]
target_sdk_version = 34
min_sdk_version = 26
[[package.metadata.android.uses_feature]]
name = "android.hardware.touchscreen"
required = true
[[package.metadata.android.uses_permission]]
name = "android.permission.INTERNET"
[package.metadata.android.application]
label = "Solitaire Quest"
# `debuggable` defaults to false on release builds; cargo-apk flips it
# automatically for debug profiles. Leaving the field unset keeps the
# default behaviour.