From 7c8271c716d7a2bf38f57c13ac48d9779e3ea8c2 Mon Sep 17 00:00:00 2001 From: Hakan Seven Date: Fri, 7 Aug 2026 17:37:59 +0300 Subject: [PATCH] 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 --- Cargo.lock | 1 + Cargo.toml | 4 ++ assets/logo.svg | 128 ++++++++++++++++++++++++++++++++----- src/app/helpers.rs | 75 ++++++++-------------- src/app/mod.rs | 3 +- src/io/file_association.rs | 10 ++- 6 files changed, 154 insertions(+), 67 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index cc262a188..25ff2fcf2 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -46,6 +46,7 @@ dependencies = [ "open", "printpdf", "rayon", + "resvg", "rfd", "rust-embed", "rustc-hash 2.1.3", diff --git a/Cargo.toml b/Cargo.toml index c654e58d5..81bedef67 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -39,6 +39,10 @@ acadrust = { version = "0.4", features = ["serde"] } dwg-thumbnailer = { path = "crates/dwg-thumbnailer" } flate2 = "1" 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-fl = "0.10" rust-embed = "8" diff --git a/assets/logo.svg b/assets/logo.svg index aa1d04098..e6bdb30b5 100644 --- a/assets/logo.svg +++ b/assets/logo.svg @@ -4,7 +4,7 @@ version="1.1" id="svg8" 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:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd" xmlns="http://www.w3.org/2000/svg" @@ -20,9 +20,9 @@ inkscape:pageopacity="0" inkscape:pagecheckerboard="0" inkscape:deskcolor="#505050" - inkscape:zoom="15.020833" - inkscape:cx="23.966713" - inkscape:cy="24" + inkscape:zoom="14.27082" + inkscape:cx="10.370813" + inkscape:cy="24.630681" inkscape:window-width="1920" inkscape:window-height="939" inkscape:window-x="0" @@ -37,18 +37,114 @@ fill="#B03020" id="rect1" /> - - - - + + + + - - - + + + - - - - - + + + + + diff --git a/src/app/helpers.rs b/src/app/helpers.rs index 94301642f..c273b5230 100644 --- a/src/app/helpers.rs +++ b/src/app/helpers.rs @@ -481,54 +481,33 @@ pub(super) fn title_case_word(value: &str) -> String { // ── 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"))] -pub(super) fn build_window_icon() -> Vec { - const W: usize = 32; - const SZ: usize = W * W * 4; +pub(super) fn build_window_icon() -> Option> { + const W: u32 = 32; + static LOGO: &[u8] = include_bytes!("../../assets/logo.svg"); - let bg = [176u8, 48, 32, 255]; - let fg = [255u8, 255, 255, 255]; - - let mut px = vec![0u8; SZ]; - for i in 0..W * W { - px[i * 4..i * 4 + 4].copy_from_slice(&bg); - } - - fn stroke(px: &mut Vec, ax: i32, ay: i32, bx: i32, by: i32, fg: [u8; 4]) { - let steps = ((bx - ax).abs().max((by - ay).abs()) * 3).max(1); - for s in 0..=steps { - let t = s as f32 / steps as f32; - let cx = ax as f32 + (bx - ax) as f32 * t; - 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 + let tree = resvg::usvg::Tree::from_data(LOGO, &resvg::usvg::Options::default()).ok()?; + let mut pixmap = resvg::tiny_skia::Pixmap::new(W, W)?; + let size = tree.size(); + // Fit the artwork to the square without distorting it, whatever aspect the + // logo happens to have. + 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, + (W as f32 - size.height() * scale) / 2.0, + ) + .pre_scale(scale, scale); + resvg::render(&tree, transform, &mut pixmap.as_mut()); + Some(pixmap.take()) } diff --git a/src/app/mod.rs b/src/app/mod.rs index 20a923ab1..acf0f920b 100644 --- a/src/app/mod.rs +++ b/src/app/mod.rs @@ -3405,7 +3405,8 @@ impl OpenCADStudio { let state = Self::new(); let (id, open_task) = window::open(window::Settings { 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, // 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 diff --git a/src/io/file_association.rs b/src/io/file_association.rs index 6b4eeba68..9a2d111fa 100644 --- a/src/io/file_association.rs +++ b/src/io/file_association.rs @@ -514,6 +514,14 @@ mod linux_impl { 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) // desktop-database refresh. 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::write(&desktop_path, &contents).map_err(|e| e.to_string())?; - install_icon()?; - // Best-effort: refresh the MIME→app cache. Absent on minimal systems. let _ = std::process::Command::new("update-desktop-database") .arg(&apps)