fix(icon): draw every icon from the logo file
Two places still showed the old mark after the logo was redrawn. The window icon was built by drawing the letterforms stroke by stroke in code, background colour and all, which made it the one icon in the application not coming from assets/logo.svg. It now rasterises that file, fitted to the square without distorting whatever aspect the logo has. It returns nothing when the logo cannot be rendered, so the window falls back to the platform default instead of to something invented here -- which is how it ended up stale in the first place. The icon installed into the icon theme was only ever written from inside the desktop-entry write, behind that write's own "nothing changed" early return. The entry's text does not change when the logo does, so once it had been written the icon was never refreshed again. The two change for different reasons; the icon is now checked first and on its own terms. It already compares bytes, so an unchanged logo still costs only a read. Worth knowing on Wayland: the compositor ignores the window icon entirely and takes the dock icon from the desktop entry, so there the installed theme file is the only one that matters -- and it refreshes when the file associations are next set up, not at launch. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
parent
36e328f7d7
commit
7c8271c716
6 changed files with 155 additions and 68 deletions
1
Cargo.lock
generated
1
Cargo.lock
generated
|
|
@ -46,6 +46,7 @@ dependencies = [
|
||||||
"open",
|
"open",
|
||||||
"printpdf",
|
"printpdf",
|
||||||
"rayon",
|
"rayon",
|
||||||
|
"resvg",
|
||||||
"rfd",
|
"rfd",
|
||||||
"rust-embed",
|
"rust-embed",
|
||||||
"rustc-hash 2.1.3",
|
"rustc-hash 2.1.3",
|
||||||
|
|
|
||||||
|
|
@ -39,6 +39,10 @@ acadrust = { version = "0.4", features = ["serde"] }
|
||||||
dwg-thumbnailer = { path = "crates/dwg-thumbnailer" }
|
dwg-thumbnailer = { path = "crates/dwg-thumbnailer" }
|
||||||
flate2 = "1"
|
flate2 = "1"
|
||||||
image = { version = "0.25", default-features = false, features = ["png", "jpeg", "bmp", "tiff"] }
|
image = { version = "0.25", default-features = false, features = ["png", "jpeg", "bmp", "tiff"] }
|
||||||
|
# Rasterises assets/logo.svg into the window icon at startup, so the taskbar
|
||||||
|
# icon comes from the same file every other build target does. Already in the
|
||||||
|
# tree behind iced's svg feature; named here only to use it directly.
|
||||||
|
resvg = { version = "0.45", default-features = false }
|
||||||
i18n-embed = { version = "0.16", features = ["fluent-system"] }
|
i18n-embed = { version = "0.16", features = ["fluent-system"] }
|
||||||
i18n-embed-fl = "0.10"
|
i18n-embed-fl = "0.10"
|
||||||
rust-embed = "8"
|
rust-embed = "8"
|
||||||
|
|
|
||||||
128
assets/logo.svg
128
assets/logo.svg
|
|
@ -4,7 +4,7 @@
|
||||||
version="1.1"
|
version="1.1"
|
||||||
id="svg8"
|
id="svg8"
|
||||||
sodipodi:docname="logo.svg"
|
sodipodi:docname="logo.svg"
|
||||||
inkscape:version="1.4.3 (0d15f75042, 2025-12-25)"
|
inkscape:version="1.4.4 (dcaf3e7d9e, 2026-05-05)"
|
||||||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||||
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
|
@ -20,9 +20,9 @@
|
||||||
inkscape:pageopacity="0"
|
inkscape:pageopacity="0"
|
||||||
inkscape:pagecheckerboard="0"
|
inkscape:pagecheckerboard="0"
|
||||||
inkscape:deskcolor="#505050"
|
inkscape:deskcolor="#505050"
|
||||||
inkscape:zoom="15.020833"
|
inkscape:zoom="14.27082"
|
||||||
inkscape:cx="23.966713"
|
inkscape:cx="10.370813"
|
||||||
inkscape:cy="24"
|
inkscape:cy="24.630681"
|
||||||
inkscape:window-width="1920"
|
inkscape:window-width="1920"
|
||||||
inkscape:window-height="939"
|
inkscape:window-height="939"
|
||||||
inkscape:window-x="0"
|
inkscape:window-x="0"
|
||||||
|
|
@ -37,18 +37,114 @@
|
||||||
fill="#B03020"
|
fill="#B03020"
|
||||||
id="rect1" />
|
id="rect1" />
|
||||||
<!-- O -->
|
<!-- O -->
|
||||||
<line x1="5" y1="10" x2="13" y2="10" stroke="white" stroke-width="3" stroke-linecap="round" id="o-top" />
|
<line
|
||||||
<line x1="5" y1="38" x2="13" y2="38" stroke="white" stroke-width="3" stroke-linecap="round" id="o-bot" />
|
x1="6.8919725"
|
||||||
<line x1="5" y1="10" x2="5" y2="38" stroke="white" stroke-width="3" stroke-linecap="round" id="o-left" />
|
y1="9.7197075"
|
||||||
<line x1="13" y1="10" x2="13" y2="38" stroke="white" stroke-width="3" stroke-linecap="round" id="o-right" />
|
x2="14.891973"
|
||||||
|
y2="9.7197075"
|
||||||
|
stroke="#ffffff"
|
||||||
|
stroke-width="3"
|
||||||
|
stroke-linecap="round"
|
||||||
|
id="o-top" />
|
||||||
|
<line
|
||||||
|
x1="6.8919725"
|
||||||
|
y1="37.719707"
|
||||||
|
x2="14.891973"
|
||||||
|
y2="37.719707"
|
||||||
|
stroke="#ffffff"
|
||||||
|
stroke-width="3"
|
||||||
|
stroke-linecap="round"
|
||||||
|
id="o-bot" />
|
||||||
|
<line
|
||||||
|
x1="6.8919725"
|
||||||
|
y1="9.7197075"
|
||||||
|
x2="6.8919725"
|
||||||
|
y2="37.719707"
|
||||||
|
stroke="#ffffff"
|
||||||
|
stroke-width="3"
|
||||||
|
stroke-linecap="round"
|
||||||
|
id="o-left" />
|
||||||
|
<line
|
||||||
|
x1="14.891973"
|
||||||
|
y1="9.7197075"
|
||||||
|
x2="14.891973"
|
||||||
|
y2="37.719707"
|
||||||
|
stroke="#ffffff"
|
||||||
|
stroke-width="3"
|
||||||
|
stroke-linecap="round"
|
||||||
|
id="o-right" />
|
||||||
<!-- C -->
|
<!-- C -->
|
||||||
<line x1="18" y1="10" x2="26" y2="10" stroke="white" stroke-width="3" stroke-linecap="round" id="c-top" />
|
<line
|
||||||
<line x1="18" y1="38" x2="26" y2="38" stroke="white" stroke-width="3" stroke-linecap="round" id="c-bot" />
|
x1="19.891972"
|
||||||
<line x1="18" y1="10" x2="18" y2="38" stroke="white" stroke-width="3" stroke-linecap="round" id="c-left" />
|
y1="9.7197075"
|
||||||
|
x2="27.891972"
|
||||||
|
y2="9.7197075"
|
||||||
|
stroke="#ffffff"
|
||||||
|
stroke-width="3"
|
||||||
|
stroke-linecap="round"
|
||||||
|
id="c-top" />
|
||||||
|
<line
|
||||||
|
x1="19.891972"
|
||||||
|
y1="37.719707"
|
||||||
|
x2="27.891972"
|
||||||
|
y2="37.719707"
|
||||||
|
stroke="#ffffff"
|
||||||
|
stroke-width="3"
|
||||||
|
stroke-linecap="round"
|
||||||
|
id="c-bot" />
|
||||||
|
<line
|
||||||
|
x1="19.891972"
|
||||||
|
y1="9.7197075"
|
||||||
|
x2="19.891972"
|
||||||
|
y2="37.719707"
|
||||||
|
stroke="#ffffff"
|
||||||
|
stroke-width="3"
|
||||||
|
stroke-linecap="round"
|
||||||
|
id="c-left" />
|
||||||
<!-- S -->
|
<!-- S -->
|
||||||
<line x1="31" y1="10" x2="39" y2="10" stroke="white" stroke-width="3" stroke-linecap="round" id="s-top" />
|
<line
|
||||||
<line x1="31" y1="10" x2="31" y2="24" stroke="white" stroke-width="3" stroke-linecap="round" id="s-tl" />
|
x1="32.891972"
|
||||||
<line x1="31" y1="24" x2="39" y2="24" stroke="white" stroke-width="3" stroke-linecap="round" id="s-mid" />
|
y1="9.7197075"
|
||||||
<line x1="39" y1="24" x2="39" y2="38" stroke="white" stroke-width="3" stroke-linecap="round" id="s-br" />
|
x2="40.891972"
|
||||||
<line x1="31" y1="38" x2="39" y2="38" stroke="white" stroke-width="3" stroke-linecap="round" id="s-bot" />
|
y2="9.7197075"
|
||||||
|
stroke="#ffffff"
|
||||||
|
stroke-width="3"
|
||||||
|
stroke-linecap="round"
|
||||||
|
id="s-top" />
|
||||||
|
<line
|
||||||
|
x1="32.891972"
|
||||||
|
y1="9.7197075"
|
||||||
|
x2="32.891972"
|
||||||
|
y2="23.719707"
|
||||||
|
stroke="#ffffff"
|
||||||
|
stroke-width="3"
|
||||||
|
stroke-linecap="round"
|
||||||
|
id="s-tl" />
|
||||||
|
<line
|
||||||
|
x1="32.891972"
|
||||||
|
y1="23.719707"
|
||||||
|
x2="40.891972"
|
||||||
|
y2="23.719707"
|
||||||
|
stroke="#ffffff"
|
||||||
|
stroke-width="3"
|
||||||
|
stroke-linecap="round"
|
||||||
|
id="s-mid" />
|
||||||
|
<line
|
||||||
|
x1="40.891972"
|
||||||
|
y1="23.719707"
|
||||||
|
x2="40.891972"
|
||||||
|
y2="37.719707"
|
||||||
|
stroke="#ffffff"
|
||||||
|
stroke-width="3"
|
||||||
|
stroke-linecap="round"
|
||||||
|
id="s-br" />
|
||||||
|
<line
|
||||||
|
x1="32.891972"
|
||||||
|
y1="37.719707"
|
||||||
|
x2="40.891972"
|
||||||
|
y2="37.719707"
|
||||||
|
stroke="#ffffff"
|
||||||
|
stroke-width="3"
|
||||||
|
stroke-linecap="round"
|
||||||
|
id="s-bot" />
|
||||||
</svg>
|
</svg>
|
||||||
|
|
|
||||||
|
Before Width: | Height: | Size: 2.4 KiB After Width: | Height: | Size: 3.2 KiB |
|
|
@ -481,54 +481,33 @@ pub(super) fn title_case_word(value: &str) -> String {
|
||||||
|
|
||||||
// ── Window icon ────────────────────────────────────────────────────────────
|
// ── Window icon ────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
/// Builds a 32×32 RGBA icon: red background with OCS drawn in white pixels.
|
/// The window icon, rasterised from the same file every other build target
|
||||||
|
/// draws its icon from.
|
||||||
|
///
|
||||||
|
/// This used to redraw the mark stroke by stroke in code, with the background
|
||||||
|
/// colour written out as a literal. The taskbar therefore kept showing whatever
|
||||||
|
/// the logo used to be, however many times the logo itself was redrawn — the
|
||||||
|
/// one icon in the application that did not come from `assets/logo.svg`.
|
||||||
|
///
|
||||||
|
/// Returns 32×32 RGBA. A logo that cannot be rendered leaves the window with
|
||||||
|
/// the platform default rather than something invented here, which would put
|
||||||
|
/// this back where it started.
|
||||||
#[cfg(not(target_arch = "wasm32"))]
|
#[cfg(not(target_arch = "wasm32"))]
|
||||||
pub(super) fn build_window_icon() -> Vec<u8> {
|
pub(super) fn build_window_icon() -> Option<Vec<u8>> {
|
||||||
const W: usize = 32;
|
const W: u32 = 32;
|
||||||
const SZ: usize = W * W * 4;
|
static LOGO: &[u8] = include_bytes!("../../assets/logo.svg");
|
||||||
|
|
||||||
let bg = [176u8, 48, 32, 255];
|
let tree = resvg::usvg::Tree::from_data(LOGO, &resvg::usvg::Options::default()).ok()?;
|
||||||
let fg = [255u8, 255, 255, 255];
|
let mut pixmap = resvg::tiny_skia::Pixmap::new(W, W)?;
|
||||||
|
let size = tree.size();
|
||||||
let mut px = vec![0u8; SZ];
|
// Fit the artwork to the square without distorting it, whatever aspect the
|
||||||
for i in 0..W * W {
|
// logo happens to have.
|
||||||
px[i * 4..i * 4 + 4].copy_from_slice(&bg);
|
let scale = (W as f32 / size.width()).min(W as f32 / size.height());
|
||||||
}
|
let transform = resvg::tiny_skia::Transform::from_translate(
|
||||||
|
(W as f32 - size.width() * scale) / 2.0,
|
||||||
fn stroke(px: &mut Vec<u8>, ax: i32, ay: i32, bx: i32, by: i32, fg: [u8; 4]) {
|
(W as f32 - size.height() * scale) / 2.0,
|
||||||
let steps = ((bx - ax).abs().max((by - ay).abs()) * 3).max(1);
|
)
|
||||||
for s in 0..=steps {
|
.pre_scale(scale, scale);
|
||||||
let t = s as f32 / steps as f32;
|
resvg::render(&tree, transform, &mut pixmap.as_mut());
|
||||||
let cx = ax as f32 + (bx - ax) as f32 * t;
|
Some(pixmap.take())
|
||||||
let cy = ay as f32 + (by - ay) as f32 * t;
|
|
||||||
for dy in -1i32..=1 {
|
|
||||||
for dx in -1i32..=1 {
|
|
||||||
let ix = cx.round() as i32 + dx;
|
|
||||||
let iy = cy.round() as i32 + dy;
|
|
||||||
if ix >= 0 && ix < W as i32 && iy >= 0 && iy < W as i32 {
|
|
||||||
let idx = (iy as usize * W + ix as usize) * 4;
|
|
||||||
px[idx..idx + 4].copy_from_slice(&fg);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// O
|
|
||||||
stroke(&mut px, 3, 6, 9, 6, fg);
|
|
||||||
stroke(&mut px, 3, 25, 9, 25, fg);
|
|
||||||
stroke(&mut px, 3, 6, 3, 25, fg);
|
|
||||||
stroke(&mut px, 9, 6, 9, 25, fg);
|
|
||||||
// C
|
|
||||||
stroke(&mut px, 12, 6, 18, 6, fg);
|
|
||||||
stroke(&mut px, 12, 25, 18, 25, fg);
|
|
||||||
stroke(&mut px, 12, 6, 12, 25, fg);
|
|
||||||
// S
|
|
||||||
stroke(&mut px, 21, 6, 27, 6, fg);
|
|
||||||
stroke(&mut px, 21, 6, 21, 15, fg);
|
|
||||||
stroke(&mut px, 21, 15, 27, 15, fg);
|
|
||||||
stroke(&mut px, 27, 15, 27, 25, fg);
|
|
||||||
stroke(&mut px, 21, 25, 27, 25, fg);
|
|
||||||
|
|
||||||
px
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -3405,7 +3405,8 @@ impl OpenCADStudio {
|
||||||
let state = Self::new();
|
let state = Self::new();
|
||||||
let (id, open_task) = window::open(window::Settings {
|
let (id, open_task) = window::open(window::Settings {
|
||||||
maximized: true,
|
maximized: true,
|
||||||
icon: window::icon::from_rgba(build_window_icon(), 32, 32).ok(),
|
icon: build_window_icon()
|
||||||
|
.and_then(|rgba| window::icon::from_rgba(rgba, 32, 32).ok()),
|
||||||
exit_on_close_request: false,
|
exit_on_close_request: false,
|
||||||
// A Wayland compositor has no StartupWMClass to go on: it resolves a
|
// A Wayland compositor has no StartupWMClass to go on: it resolves a
|
||||||
// window's dock icon by matching the window's app_id against the
|
// window's dock icon by matching the window's app_id against the
|
||||||
|
|
|
||||||
|
|
@ -514,6 +514,14 @@ mod linux_impl {
|
||||||
StartupWMClass=OpenCADStudio\n"
|
StartupWMClass=OpenCADStudio\n"
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// The icon is checked first and on its own. It and the .desktop entry
|
||||||
|
// change for different reasons — the entry only when this text does,
|
||||||
|
// the icon whenever the logo is redrawn — so gating the icon on the
|
||||||
|
// entry meant a new logo never reached the theme after the entry had
|
||||||
|
// been written once. `install_icon` compares bytes itself, so this
|
||||||
|
// costs a read when nothing has changed.
|
||||||
|
install_icon()?;
|
||||||
|
|
||||||
// Nothing changed since last launch → skip the write and the (slow)
|
// Nothing changed since last launch → skip the write and the (slow)
|
||||||
// desktop-database refresh.
|
// desktop-database refresh.
|
||||||
if std::fs::read_to_string(&desktop_path).ok().as_deref() == Some(contents.as_str()) {
|
if std::fs::read_to_string(&desktop_path).ok().as_deref() == Some(contents.as_str()) {
|
||||||
|
|
@ -523,8 +531,6 @@ mod linux_impl {
|
||||||
std::fs::create_dir_all(&apps).map_err(|e| e.to_string())?;
|
std::fs::create_dir_all(&apps).map_err(|e| e.to_string())?;
|
||||||
std::fs::write(&desktop_path, &contents).map_err(|e| e.to_string())?;
|
std::fs::write(&desktop_path, &contents).map_err(|e| e.to_string())?;
|
||||||
|
|
||||||
install_icon()?;
|
|
||||||
|
|
||||||
// Best-effort: refresh the MIME→app cache. Absent on minimal systems.
|
// Best-effort: refresh the MIME→app cache. Absent on minimal systems.
|
||||||
let _ = std::process::Command::new("update-desktop-database")
|
let _ = std::process::Command::new("update-desktop-database")
|
||||||
.arg(&apps)
|
.arg(&apps)
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue