mirror of
https://github.com/LGUG2Z/komorebi.git
synced 2026-11-15 13:22:19 +01:00
feat(icon): a watermelon for every window
The recorder window and the viewer carry an icon of their own: a slice of watermelon, drawn as komorebi-recorder/assets/watermelon.svg. `just icon` (assets/generate.py, with resvg from the devShell) renders the PNG the windows set and the ICO that Windows embeds. Until now eframe's default egui icon showed. macOS: eframe sets the PNG as the Dock icon while a window is open. The program is a bare executable and not an app bundle, so Finder is unchanged. Windows: eframe sets the same PNG as the window and taskbar icon. build.rs compiles the ICO into both executables with embed-resource (rc.exe on Windows, llvm-rc when cross-building from a Mac), and the installer shows it in Apps & features through ARPPRODUCTICON. The taskbar, Explorer and the installer have not been looked at on Windows: W4 and W5 in docs/dev/windows-checkpoints.md now say what to check. embed-resource adds seven crates to the lockfile and moves toml_datetime and toml_parser up by a patch release.
This commit is contained in:
1 parent
df4ab2d2a5
commit
8a203f0da7
19 files changed
+335
-17
No files matched your search
+5
-2
@@ -47,8 +47,8 @@ OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
||||
## Development environment
|
||||
|
||||
The project is built with Nix. `nix develop` (or direnv, whose `.envrc` is `use flake`) provides the
|
||||
Rust toolchain with nightly `rustfmt`, `cargo-deny`, `cargo-nextest`, `ffmpeg`, `jq`, `just` and
|
||||
`ripgrep`, and sets `KOMOREBI_MAC_SRC` and `KOMOREBI_WINDOWS_SRC` (with `_COMMIT`) to the komorebi
|
||||
Rust toolchain with nightly `rustfmt`, `cargo-deny`, `cargo-nextest`, `ffmpeg`, `jq`, `just`,
|
||||
`resvg` and `ripgrep`, and sets `KOMOREBI_MAC_SRC` and `KOMOREBI_WINDOWS_SRC` (with `_COMMIT`) to the komorebi
|
||||
sources pinned in `flake.lock`. The tests that read komorebi's source need them, so run `cargo` inside
|
||||
the shell.
|
||||
|
||||
@@ -58,6 +58,9 @@ The Windows code is type-checked from a Mac with `nix develop .#windows`, whose
|
||||
On a Windows machine, the steps in [docs/dev/windows-checkpoints.md](docs/dev/windows-checkpoints.md)
|
||||
say what to run and what to look for.
|
||||
|
||||
The icon is `komorebi-recorder/assets/watermelon.svg`. After changing it, run `just icon` to render the PNG that
|
||||
the windows set and the ICO that Windows embeds, and commit the three together.
|
||||
|
||||
## Checks
|
||||
|
||||
Before pushing, run
|
||||
|
||||
Generated
+89
-14
@@ -215,7 +215,7 @@ version = "1.1.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "40c48f72fd53cd289104fc64099abca73db4166ad86ea0b4341abe65af83dadc"
|
||||
dependencies = [
|
||||
"windows-sys 0.60.2",
|
||||
"windows-sys 0.61.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -226,7 +226,7 @@ checksum = "291e6a250ff86cd4a820112fb8898808a366d8f9f58ce16d1f538353ad55747d"
|
||||
dependencies = [
|
||||
"anstyle",
|
||||
"once_cell_polyfill",
|
||||
"windows-sys 0.60.2",
|
||||
"windows-sys 0.61.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -928,7 +928,7 @@ dependencies = [
|
||||
"libc",
|
||||
"option-ext",
|
||||
"redox_users",
|
||||
"windows-sys 0.59.0",
|
||||
"windows-sys 0.61.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -1133,6 +1133,20 @@ dependencies = [
|
||||
"bytemuck",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "embed-resource"
|
||||
version = "3.0.12"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6168487b5e7eb2673e6efe1200b1848016e436a6ca0b8031bf04bd9d493a249f"
|
||||
dependencies = [
|
||||
"cc",
|
||||
"memchr",
|
||||
"rustc_version",
|
||||
"toml",
|
||||
"vswhom",
|
||||
"winreg",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "endi"
|
||||
version = "1.1.1"
|
||||
@@ -1197,7 +1211,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"windows-sys 0.52.0",
|
||||
"windows-sys 0.61.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -1918,6 +1932,7 @@ dependencies = [
|
||||
"dispatch2",
|
||||
"dunce",
|
||||
"eframe",
|
||||
"embed-resource",
|
||||
"epaint_default_fonts",
|
||||
"libc",
|
||||
"objc2 0.6.4",
|
||||
@@ -3030,7 +3045,7 @@ dependencies = [
|
||||
"errno",
|
||||
"libc",
|
||||
"linux-raw-sys 0.12.1",
|
||||
"windows-sys 0.52.0",
|
||||
"windows-sys 0.61.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -3187,6 +3202,15 @@ dependencies = [
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde_spanned"
|
||||
version = "1.1.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4e7523beb55eece201a2356bee0bbca0d1ab466c14c07703b2e0ee6d42cb0c2c"
|
||||
dependencies = [
|
||||
"serde_core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "sharded-slab"
|
||||
version = "0.1.7"
|
||||
@@ -3406,7 +3430,7 @@ dependencies = [
|
||||
"getrandom 0.4.2",
|
||||
"once_cell",
|
||||
"rustix 1.1.4",
|
||||
"windows-sys 0.52.0",
|
||||
"windows-sys 0.61.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -3425,7 +3449,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "230a1b821ccbd75b185820a1f1ff7b14d21da1e442e22c0863ea5f08771a8874"
|
||||
dependencies = [
|
||||
"rustix 1.1.4",
|
||||
"windows-sys 0.59.0",
|
||||
"windows-sys 0.61.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -3527,10 +3551,25 @@ dependencies = [
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "toml_datetime"
|
||||
version = "1.1.1+spec-1.1.0"
|
||||
name = "toml"
|
||||
version = "1.1.8+spec-1.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3165f65f62e28e0115a00b2ebdd37eb6f3b641855f9d636d3cd4103767159ad7"
|
||||
checksum = "20489e00e4d8741d6be680764cc12e270655e375a20d1011e844a9c3379e678d"
|
||||
dependencies = [
|
||||
"indexmap",
|
||||
"serde_core",
|
||||
"serde_spanned",
|
||||
"toml_datetime",
|
||||
"toml_parser",
|
||||
"toml_writer",
|
||||
"winnow",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "toml_datetime"
|
||||
version = "1.1.2+spec-1.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2b86d767906c6c42421dcba507eb9d203e779497710a47782a224bb871653053"
|
||||
dependencies = [
|
||||
"serde_core",
|
||||
]
|
||||
@@ -3549,13 +3588,19 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "toml_parser"
|
||||
version = "1.1.2+spec-1.1.0"
|
||||
version = "1.1.5+spec-1.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a2abe9b86193656635d2411dc43050282ca48aa31c2451210f4202550afb7526"
|
||||
checksum = "baa693a8032d7e1cada7d0041e96126df243179ff061456783ac7f12bda4744c"
|
||||
dependencies = [
|
||||
"winnow",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "toml_writer"
|
||||
version = "1.1.3+spec-1.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "06bdbd8cfc056b8d2e2e85f29b56a3bdbecb527cef81eb39e3e7b98af4652770"
|
||||
|
||||
[[package]]
|
||||
name = "tracing"
|
||||
version = "0.1.44"
|
||||
@@ -3633,7 +3678,7 @@ checksum = "f2f6fb2847f6742cd76af783a2a2c49e9375d0a111c7bef6f71cd9e738c72d6e"
|
||||
dependencies = [
|
||||
"memoffset",
|
||||
"tempfile",
|
||||
"windows-sys 0.60.2",
|
||||
"windows-sys 0.61.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -3707,6 +3752,26 @@ version = "0.9.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a"
|
||||
|
||||
[[package]]
|
||||
name = "vswhom"
|
||||
version = "0.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "be979b7f07507105799e854203b470ff7c78a1639e330a58f183b5fea574608b"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"vswhom-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "vswhom-sys"
|
||||
version = "0.1.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "fb067e4cbd1ff067d1df46c9194b5de0e98efd2810bbc95c5d5e5f25a3231150"
|
||||
dependencies = [
|
||||
"cc",
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "walkdir"
|
||||
version = "2.5.0"
|
||||
@@ -4180,7 +4245,7 @@ version = "0.1.11"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22"
|
||||
dependencies = [
|
||||
"windows-sys 0.52.0",
|
||||
"windows-sys 0.61.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -4678,6 +4743,16 @@ dependencies = [
|
||||
"memchr",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "winreg"
|
||||
version = "0.56.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7d6f32a0ff4a9f6f01231eb2059cc85479330739333e0e58cadf03b6af2cca10"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"windows-sys 0.61.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wit-bindgen"
|
||||
version = "0.51.0"
|
||||
|
||||
@@ -117,6 +117,7 @@ The recorder window and the viewer must not be in the video they are recording,
|
||||
|---|---|---|
|
||||
| left out of the video | by name: every process called `komorebi-recorder` is excluded from the capture | each window asks to be left out of captures with `WDA_EXCLUDEFROMCAPTURE` (Windows 10 version 2004 and later) |
|
||||
| left alone by komorebi | the windows have no title, which komorebi does not manage | the same, and `KOMOREBI_RECORDER_PROBE_WINDOW=1` makes a window ask komorebi whether it would manage it and write the answer to `komorebi-recorder-window-probe.txt` in the temp directory |
|
||||
| icon | a slice of watermelon, which the Dock shows while a window is open (eframe sets it as the application's icon image). The program is a bare executable and not an app bundle, so Finder gives the file the generic executable icon | the same picture as the window and taskbar icon while a window is open, and compiled into both executables as a resource, which is what Explorer shows for the files; the installer shows it beside the program in Apps & features |
|
||||
| executables | `komorebi-recorder` | `komorebi-recorder.exe` (console: run commands with this one) and `komorebi-recorder-gui.exe` (no console window) |
|
||||
|
||||
## Degraded modes
|
||||
|
||||
@@ -179,6 +179,7 @@ Status: can be run once W1 shows capture works.
|
||||
1. Start komorebi, then open the viewer on a Windows session: `komorebi-recorder-gui.exe view
|
||||
C:\temp\w2`. It should open floating (not tiled into your layout) and be usable: moving, resizing,
|
||||
minimizing and closing it. If komorebi tiles it, say so.
|
||||
Its taskbar button and Alt+Tab entry should show the watermelon icon.
|
||||
2. Ask komorebi what it thinks of it. Set `KOMOREBI_RECORDER_PROBE_WINDOW=1` before starting the
|
||||
viewer (or the recorder window), wait a few seconds, and send back
|
||||
`%TEMP%\komorebi-recorder-window-probe.txt`.
|
||||
@@ -204,6 +205,7 @@ warn on the first run.
|
||||
|
||||
That it runs proves the `PATH` entry. `Program Files\komorebi-recorder\bin` should hold `komorebi-recorder.exe` and
|
||||
`komorebi-recorder-gui.exe`.
|
||||
In Explorer both executables should show the watermelon icon, and so should the entry in Apps & features.
|
||||
|
||||
2. Install a second build over it, such as the next nightly. It should upgrade in place: one entry in Apps & features,
|
||||
not two.
|
||||
|
||||
@@ -58,7 +58,9 @@
|
||||
|| (pkgs.lib.hasSuffix "/komorebi-recorder/src/guide.md" path)
|
||||
|| (pkgs.lib.hasInfix "/komorebi-recorder/tests/fixtures/" path)
|
||||
# the installer, which `tests/packaging.rs` checks
|
||||
|| (pkgs.lib.hasInfix "/wix/" path);
|
||||
|| (pkgs.lib.hasInfix "/wix/" path)
|
||||
# the icon, which `src/icon.rs` embeds and `build.rs` compiles into the Windows executables
|
||||
|| (pkgs.lib.hasInfix "/komorebi-recorder/assets/" path);
|
||||
};
|
||||
|
||||
mkPackages =
|
||||
@@ -299,6 +301,8 @@
|
||||
pkgs.jq
|
||||
pkgs.just
|
||||
pkgs.ripgrep
|
||||
# `just icon` renders the watermelon
|
||||
pkgs.resvg
|
||||
];
|
||||
};
|
||||
|
||||
|
||||
@@ -56,6 +56,10 @@ record *args:
|
||||
build-windows:
|
||||
nix build .#komorebi-recorder-windows --out-link result-windows
|
||||
|
||||
# render the icon: komorebi-recorder/assets/watermelon.svg into the PNG the windows set and the ICO Windows embeds
|
||||
icon:
|
||||
python3 komorebi-recorder/assets/generate.py
|
||||
|
||||
deps:
|
||||
cargo update
|
||||
just depgen
|
||||
|
||||
@@ -144,5 +144,6 @@ windows = { version = "0.62", features = [
|
||||
] }
|
||||
|
||||
[build-dependencies]
|
||||
embed-resource = "3"
|
||||
proc-macro2 = { version = "1", features = ["span-locations"] }
|
||||
syn = { version = "2", features = ["full", "visit"] }
|
||||
@@ -0,0 +1,44 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Renders watermelon.svg into the two files the program uses.
|
||||
|
||||
watermelon.png (256 by 256) is the icon every window sets while the program runs, and
|
||||
watermelon.ico is the icon Windows shows for the executables and for the installer. Needs
|
||||
`resvg` on PATH, which `nix develop` provides; run it with `just icon`.
|
||||
"""
|
||||
|
||||
import struct
|
||||
import subprocess
|
||||
import tempfile
|
||||
from pathlib import Path
|
||||
|
||||
HERE = Path(__file__).resolve().parent
|
||||
SIZES = [16, 24, 32, 48, 64, 128, 256]
|
||||
|
||||
|
||||
def render(size, directory):
|
||||
out = Path(directory) / f"{size}.png"
|
||||
subprocess.run(
|
||||
["resvg", "-w", str(size), "-h", str(size), str(HERE / "watermelon.svg"), str(out)],
|
||||
check=True,
|
||||
)
|
||||
return out.read_bytes()
|
||||
|
||||
|
||||
def ico(images):
|
||||
"""An ICO file whose entries are PNGs, which Windows has read since Vista. A size of 256 is
|
||||
written as 0."""
|
||||
offset = 6 + 16 * len(images)
|
||||
entries, data = b"", b""
|
||||
for size, png in images:
|
||||
entries += struct.pack(
|
||||
"<BBBBHHII", size % 256, size % 256, 0, 0, 1, 32, len(png), offset + len(data)
|
||||
)
|
||||
data += png
|
||||
return struct.pack("<HHH", 0, 1, len(images)) + entries + data
|
||||
|
||||
|
||||
with tempfile.TemporaryDirectory() as directory:
|
||||
images = [(size, render(size, directory)) for size in SIZES]
|
||||
|
||||
(HERE / "watermelon.png").write_bytes(images[-1][1])
|
||||
(HERE / "watermelon.ico").write_bytes(ico(images))
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 33 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 17 KiB |
@@ -0,0 +1 @@
|
||||
1 ICON "watermelon.ico"
|
||||
@@ -0,0 +1,25 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512" width="512" height="512">
|
||||
<title>A slice of watermelon</title>
|
||||
<defs>
|
||||
<linearGradient id="flesh" x1="0" y1="0" x2="0" y2="1">
|
||||
<stop offset="0" stop-color="#ff6f7d"/>
|
||||
<stop offset="1" stop-color="#ea3a52"/>
|
||||
</linearGradient>
|
||||
</defs>
|
||||
<g transform="translate(239.1 193.8) rotate(-30) scale(1.054)">
|
||||
<path d="M-236 0 A236 236 0 0 0 236 0 Z" fill="#2a7a3d" stroke="#2a7a3d" stroke-width="18" stroke-linejoin="round"/>
|
||||
<path d="M-212 0 A212 212 0 0 0 212 0 Z" fill="#9bd36b"/>
|
||||
<path d="M-194 0 A194 194 0 0 0 194 0 Z" fill="#f3f8dc"/>
|
||||
<path d="M-182 0 A182 182 0 0 0 182 0 Z" fill="url(#flesh)"/>
|
||||
<g transform="translate(56.7 44.3) rotate(-52.0)"><path d="M0 -21 C13 -7 13 15 0 21 C-13 15 -13 -7 0 -21 Z" fill="#2b1d1d"/><ellipse cx="-4.5" cy="-1" rx="2.6" ry="7" fill="#ffffff" opacity=".28"/></g>
|
||||
<g transform="translate(22.2 68.5) rotate(-18.0)"><path d="M0 -21 C13 -7 13 15 0 21 C-13 15 -13 -7 0 -21 Z" fill="#2b1d1d"/><ellipse cx="-4.5" cy="-1" rx="2.6" ry="7" fill="#ffffff" opacity=".28"/></g>
|
||||
<g transform="translate(-22.2 68.5) rotate(18.0)"><path d="M0 -21 C13 -7 13 15 0 21 C-13 15 -13 -7 0 -21 Z" fill="#2b1d1d"/><ellipse cx="-4.5" cy="-1" rx="2.6" ry="7" fill="#ffffff" opacity=".28"/></g>
|
||||
<g transform="translate(-56.7 44.3) rotate(52.0)"><path d="M0 -21 C13 -7 13 15 0 21 C-13 15 -13 -7 0 -21 Z" fill="#2b1d1d"/><ellipse cx="-4.5" cy="-1" rx="2.6" ry="7" fill="#ffffff" opacity=".28"/></g>
|
||||
<g transform="translate(124.0 45.1) rotate(-70.0)"><path d="M0 -21 C13 -7 13 15 0 21 C-13 15 -13 -7 0 -21 Z" fill="#2b1d1d"/><ellipse cx="-4.5" cy="-1" rx="2.6" ry="7" fill="#ffffff" opacity=".28"/></g>
|
||||
<g transform="translate(91.7 95.0) rotate(-44.0)"><path d="M0 -21 C13 -7 13 15 0 21 C-13 15 -13 -7 0 -21 Z" fill="#2b1d1d"/><ellipse cx="-4.5" cy="-1" rx="2.6" ry="7" fill="#ffffff" opacity=".28"/></g>
|
||||
<g transform="translate(40.8 125.5) rotate(-18.0)"><path d="M0 -21 C13 -7 13 15 0 21 C-13 15 -13 -7 0 -21 Z" fill="#2b1d1d"/><ellipse cx="-4.5" cy="-1" rx="2.6" ry="7" fill="#ffffff" opacity=".28"/></g>
|
||||
<g transform="translate(-18.4 130.7) rotate(8.0)"><path d="M0 -21 C13 -7 13 15 0 21 C-13 15 -13 -7 0 -21 Z" fill="#2b1d1d"/><ellipse cx="-4.5" cy="-1" rx="2.6" ry="7" fill="#ffffff" opacity=".28"/></g>
|
||||
<g transform="translate(-73.8 109.4) rotate(34.0)"><path d="M0 -21 C13 -7 13 15 0 21 C-13 15 -13 -7 0 -21 Z" fill="#2b1d1d"/><ellipse cx="-4.5" cy="-1" rx="2.6" ry="7" fill="#ffffff" opacity=".28"/></g>
|
||||
<g transform="translate(-114.3 66.0) rotate(60.0)"><path d="M0 -21 C13 -7 13 15 0 21 C-13 15 -13 -7 0 -21 Z" fill="#2b1d1d"/><ellipse cx="-4.5" cy="-1" rx="2.6" ry="7" fill="#ffffff" opacity=".28"/></g>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 2.7 KiB |
@@ -20,6 +20,9 @@
|
||||
//! `KOMOREBI_WINDOWS_COMMIT` for a build outside a checkout (nix).
|
||||
//!
|
||||
//! If a source is not there the catalog is empty and says so.
|
||||
//!
|
||||
//! The script also gives the Windows executables their icon, which Explorer reads from a resource in the file: see
|
||||
//! [`embed_icon`].
|
||||
|
||||
use proc_macro2::TokenStream;
|
||||
use proc_macro2::TokenTree;
|
||||
@@ -482,11 +485,33 @@ fn index(flavor: &Flavor) -> String {
|
||||
out
|
||||
}
|
||||
|
||||
/// Compiles the icon into the executables when the target is Windows. The windows set their own icon while they run
|
||||
/// (`src/icon.rs`), which is not what Explorer shows for the file, and the command-line build has no window at all.
|
||||
///
|
||||
/// A Windows host compiles `assets/watermelon.rc` with `rc.exe`, and any other host with `llvm-rc`, as the Windows
|
||||
/// build in `flake.nix` does. `llvm-rc` reads a copy of the file from the build directory, so the icon is found by
|
||||
/// naming `assets` as an include directory and not by its place next to the `.rc`.
|
||||
fn embed_icon() {
|
||||
println!("cargo:rerun-if-changed=assets/watermelon.rc");
|
||||
println!("cargo:rerun-if-changed=assets/watermelon.ico");
|
||||
|
||||
if env::var("CARGO_CFG_TARGET_OS").is_ok_and(|target| target == "windows") {
|
||||
embed_resource::compile(
|
||||
"assets/watermelon.rc",
|
||||
embed_resource::ParamsIncludeDirs(&["assets"]),
|
||||
)
|
||||
.manifest_required()
|
||||
.expect("compile the icon resource");
|
||||
}
|
||||
}
|
||||
|
||||
fn main() {
|
||||
let out_dir = PathBuf::from(env::var("OUT_DIR").expect("OUT_DIR"));
|
||||
|
||||
println!("cargo:rerun-if-changed=build.rs");
|
||||
|
||||
embed_icon();
|
||||
|
||||
let out: String = FLAVORS.iter().map(index).collect();
|
||||
|
||||
fs::write(out_dir.join("log_catalog.rs"), out).expect("write log_catalog.rs");
|
||||
|
||||
@@ -170,6 +170,7 @@ pub fn run(args: RecordArgs) -> eyre::Result<()> {
|
||||
viewport: egui::ViewportBuilder::default()
|
||||
// no title: komorebi leaves untitled windows alone
|
||||
.with_title("")
|
||||
.with_icon(crate::icon::window_icon())
|
||||
.with_decorations(false)
|
||||
.with_inner_size([420.0, 380.0])
|
||||
.with_min_inner_size([360.0, 260.0])
|
||||
|
||||
@@ -0,0 +1,99 @@
|
||||
//! The icon of every window the program opens: a slice of watermelon.
|
||||
//!
|
||||
//! The picture is `assets/watermelon.svg`. `assets/generate.py` (`just icon`) renders it into `assets/watermelon.png`,
|
||||
//! which this module embeds, and `assets/watermelon.ico`, which Windows shows for the executables (`build.rs` compiles
|
||||
//! it into them) and for the installer. What each host does with the icon is in `docs/PLATFORM.md`.
|
||||
|
||||
use eframe::egui;
|
||||
use std::io::Cursor;
|
||||
|
||||
/// The watermelon, 256 by 256 pixels of RGBA.
|
||||
const PNG: &[u8] = include_bytes!("../assets/watermelon.png");
|
||||
|
||||
/// Returns the icon of the program's windows: a slice of watermelon on a transparent background, 256 by 256 pixels.
|
||||
///
|
||||
/// The recorder window and the viewer pass it to [`egui::ViewportBuilder::with_icon`]. eframe sets it as the window and
|
||||
/// taskbar icon on Windows and as the Dock icon on macOS.
|
||||
///
|
||||
/// The picture is part of the binary, and a test holds that it decodes. If it ever did not, this returns an empty
|
||||
/// [`egui::IconData`], which the window system takes to mean no icon: the window opens with the system's default.
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```rust
|
||||
/// let icon = komorebi_recorder::icon::window_icon();
|
||||
///
|
||||
/// assert_eq!((icon.width, icon.height), (256, 256));
|
||||
/// assert_eq!(icon.rgba.len(), 256 * 256 * 4);
|
||||
///
|
||||
/// // the corner of the picture is transparent and its middle is not
|
||||
/// assert_eq!(icon.rgba[3], 0);
|
||||
/// assert_eq!(icon.rgba[(128 * 256 + 128) * 4 + 3], 255);
|
||||
/// ```
|
||||
pub fn window_icon() -> egui::IconData {
|
||||
decode().unwrap_or_default()
|
||||
}
|
||||
|
||||
/// The picture as RGBA, or nothing if it is not an RGBA PNG with eight bits a channel, which is what `generate.py`
|
||||
/// writes.
|
||||
fn decode() -> Option<egui::IconData> {
|
||||
let mut reader = png::Decoder::new(Cursor::new(PNG)).read_info().ok()?;
|
||||
let mut rgba = vec![0; reader.output_buffer_size()?];
|
||||
let info = reader.next_frame(&mut rgba).ok()?;
|
||||
|
||||
(info.color_type == png::ColorType::Rgba && info.bit_depth == png::BitDepth::Eight).then(|| {
|
||||
rgba.truncate(info.buffer_size());
|
||||
egui::IconData {
|
||||
rgba,
|
||||
width: info.width,
|
||||
height: info.height,
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
const ICO: &[u8] = include_bytes!("../assets/watermelon.ico");
|
||||
|
||||
#[test]
|
||||
fn the_embedded_picture_decodes() {
|
||||
let icon = decode().expect("watermelon.png is an RGBA PNG with eight bits a channel");
|
||||
|
||||
assert_eq!(icon.rgba.len(), (icon.width * icon.height * 4) as usize);
|
||||
assert_eq!(window_icon(), icon);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn the_windows_icon_holds_the_same_picture_at_256_pixels() {
|
||||
assert_eq!(ICO[..4], [0, 0, 1, 0], "an icon file starts with its type");
|
||||
let count = usize::from(u16::from_le_bytes([ICO[4], ICO[5]]));
|
||||
assert!(
|
||||
count > 1,
|
||||
"one size is not enough for a taskbar and a window"
|
||||
);
|
||||
|
||||
let mut widths = Vec::new();
|
||||
for entry in ICO[6..].as_chunks::<16>().0.iter().take(count) {
|
||||
// a width of 0 means 256
|
||||
let width = entry[0];
|
||||
let pixels = if width == 0 { 256 } else { u32::from(width) };
|
||||
let length = u32::from_le_bytes([entry[8], entry[9], entry[10], entry[11]]) as usize;
|
||||
let offset = u32::from_le_bytes([entry[12], entry[13], entry[14], entry[15]]) as usize;
|
||||
let image = &ICO[offset..offset + length];
|
||||
|
||||
assert!(
|
||||
image.starts_with(b"\x89PNG"),
|
||||
"the {pixels} pixel entry is not a PNG"
|
||||
);
|
||||
if width == 0 {
|
||||
assert_eq!(image, PNG, "the 256 pixel entry is not watermelon.png");
|
||||
}
|
||||
widths.push(width);
|
||||
}
|
||||
|
||||
assert!(widths.contains(&0), "no 256 pixel entry in {widths:?}");
|
||||
assert!(widths.contains(&16), "no 16 pixel entry in {widths:?}");
|
||||
}
|
||||
}
|
||||
@@ -55,6 +55,7 @@
|
||||
//! | [`annotations`] | the bug regions, notes and findings written into a session |
|
||||
//! | [`recovery`] | rebuilding a session whose recorder died |
|
||||
//! | [`cli`] | the command line |
|
||||
//! | [`icon`] | the icon of the windows |
|
||||
//! | [`app`] | the recorder window |
|
||||
//! | [`av`] and [`selftest`] | decoding the recorded video and checking it against the clocks |
|
||||
//! | [`doctor`] | what a recording leans on, and probes that measure it |
|
||||
@@ -82,6 +83,7 @@ pub mod cli;
|
||||
pub mod clock;
|
||||
pub mod doctor;
|
||||
pub mod frames;
|
||||
pub mod icon;
|
||||
pub mod incidents;
|
||||
pub mod komorebi_link;
|
||||
pub mod lock;
|
||||
|
||||
@@ -113,6 +113,7 @@ pub fn run(session: Option<PathBuf>) -> eyre::Result<()> {
|
||||
// no title: komorebi leaves untitled windows alone, so opening the viewer does not
|
||||
// move the layout of the machine whose behavior is being investigated
|
||||
.with_title("")
|
||||
.with_icon(crate::icon::window_icon())
|
||||
.with_title_shown(false)
|
||||
.with_titlebar_shown(false)
|
||||
.with_fullsize_content_view(true)
|
||||
|
||||
@@ -138,3 +138,29 @@ fn the_license_the_installer_shows_is_there_and_is_the_projects() {
|
||||
"main.wxs does not refer to wix\\License.rtf"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn the_installer_shows_the_icon_the_executables_carry() {
|
||||
let text = installer();
|
||||
|
||||
let id = attribute(&text, "Icon", "Id");
|
||||
let source = attribute(&text, "Icon", "SourceFile");
|
||||
assert!(
|
||||
text.contains(&format!("<Property Id='ARPPRODUCTICON' Value='{id}'")),
|
||||
"Apps & features is not told to show the icon {id}"
|
||||
);
|
||||
assert!(
|
||||
repository().join(source.replace('\\', "/")).is_file(),
|
||||
"wix/main.wxs takes its icon from {source}, which is not there"
|
||||
);
|
||||
|
||||
// the resource build.rs compiles into the executables names the same file
|
||||
let resource =
|
||||
std::fs::read_to_string(Path::new(env!("CARGO_MANIFEST_DIR")).join("assets/watermelon.rc"))
|
||||
.expect("assets/watermelon.rc");
|
||||
let icon = source.rsplit('\\').next().expect("a file name");
|
||||
assert!(
|
||||
resource.contains(&format!("\"{icon}\"")),
|
||||
"the executables carry an icon other than the installer's {icon}: {resource}"
|
||||
);
|
||||
}
|
||||
@@ -112,6 +112,10 @@
|
||||
|
||||
<Property Id='ARPHELPLINK' Value='https://github.com/LGUG2Z/komorebi-recorder' />
|
||||
|
||||
<!-- the icon of the entry in Apps & features; the executables carry the same one -->
|
||||
<Icon Id='watermelon.ico' SourceFile='komorebi-recorder\assets\watermelon.ico' />
|
||||
<Property Id='ARPPRODUCTICON' Value='watermelon.ico' />
|
||||
|
||||
<UI>
|
||||
<UIRef Id='WixUI_FeatureTree' />
|
||||
</UI>
|
||||
|
||||
Reference in new issue
Block a user