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:
Hakan Seven 2026-08-07 17:37:59 +03:00
commit 7c8271c716
6 changed files with 155 additions and 68 deletions

1
Cargo.lock generated
View file

@ -46,6 +46,7 @@ dependencies = [
"open",
"printpdf",
"rayon",
"resvg",
"rfd",
"rust-embed",
"rustc-hash 2.1.3",

View file

@ -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"

View file

@ -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" />
<!-- O -->
<line x1="5" y1="10" x2="13" y2="10" stroke="white" stroke-width="3" stroke-linecap="round" id="o-top" />
<line x1="5" y1="38" x2="13" y2="38" stroke="white" stroke-width="3" stroke-linecap="round" id="o-bot" />
<line x1="5" y1="10" x2="5" y2="38" stroke="white" stroke-width="3" stroke-linecap="round" id="o-left" />
<line x1="13" y1="10" x2="13" y2="38" stroke="white" stroke-width="3" stroke-linecap="round" id="o-right" />
<line
x1="6.8919725"
y1="9.7197075"
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 -->
<line x1="18" y1="10" x2="26" y2="10" stroke="white" stroke-width="3" stroke-linecap="round" id="c-top" />
<line x1="18" y1="38" x2="26" y2="38" stroke="white" stroke-width="3" stroke-linecap="round" id="c-bot" />
<line x1="18" y1="10" x2="18" y2="38" stroke="white" stroke-width="3" stroke-linecap="round" id="c-left" />
<line
x1="19.891972"
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 -->
<line x1="31" y1="10" x2="39" y2="10" stroke="white" stroke-width="3" stroke-linecap="round" id="s-top" />
<line x1="31" y1="10" x2="31" y2="24" stroke="white" stroke-width="3" stroke-linecap="round" id="s-tl" />
<line x1="31" y1="24" x2="39" y2="24" stroke="white" stroke-width="3" stroke-linecap="round" id="s-mid" />
<line x1="39" y1="24" x2="39" y2="38" stroke="white" stroke-width="3" stroke-linecap="round" id="s-br" />
<line x1="31" y1="38" x2="39" y2="38" stroke="white" stroke-width="3" stroke-linecap="round" id="s-bot" />
<line
x1="32.891972"
y1="9.7197075"
x2="40.891972"
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>

Before

Width:  |  Height:  |  Size: 2.4 KiB

After

Width:  |  Height:  |  Size: 3.2 KiB

Before After
Before After

View file

@ -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<u8> {
const W: usize = 32;
const SZ: usize = W * W * 4;
pub(super) fn build_window_icon() -> Option<Vec<u8>> {
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<u8>, 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())
}

View file

@ -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

View file

@ -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)