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)