From 3f82c455205ccc5d453e959cd6056e0a09825ef7 Mon Sep 17 00:00:00 2001 From: Klemen Date: Wed, 22 Apr 2026 03:20:13 +0200 Subject: [PATCH] feat: windows app icon (#3753) * default icon * cli * fix typos, fmt + icon fix I don't think it's a good idea to do [default.extend-words] fo = "fo" as it's a very common mispelling for 'for' * fix clippy * return error on non implemented platforms instead of default dx icon * fix * fix fix the amazing merge renamed winres to windows moved windows winres build request to windows * fix typos * fix schema * Update schema.json * fix schema again * use default icon if no fallback * wire up the rest of windows * fmt --------- Co-authored-by: Jonathan Kelley --- Cargo.lock | 2 + _typos.toml | 2 +- packages/cli/assets/icon.ico | Bin 0 -> 112500 bytes packages/cli/schema.json | 68 ++ packages/cli/src/build/mod.rs | 1 + packages/cli/src/build/request.rs | 16 +- packages/cli/src/build/windows.rs | 779 +++++++++++++++++++++++ packages/cli/src/cli/bundle.rs | 5 +- packages/cli/src/config/app.rs | 4 + packages/cli/src/config/bundle.rs | 40 ++ packages/cli/src/config/dioxus_config.rs | 1 + packages/desktop/Cargo.toml | 2 + packages/desktop/src/default_icon.rs | 187 ++++++ packages/desktop/src/lib.rs | 2 + packages/desktop/src/menubar.rs | 5 + packages/desktop/src/trayicon.rs | 27 +- packages/desktop/src/webview.rs | 9 +- 17 files changed, 1125 insertions(+), 25 deletions(-) create mode 100644 packages/cli/assets/icon.ico create mode 100644 packages/cli/src/build/windows.rs create mode 100644 packages/desktop/src/default_icon.rs diff --git a/Cargo.lock b/Cargo.lock index 8138d5d44..4481ac402 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3578,6 +3578,7 @@ version = "0.7.5" name = "dioxus-desktop" version = "0.7.5" dependencies = [ + "anyhow", "async-trait", "base64 0.22.1", "bytes", @@ -3602,6 +3603,7 @@ dependencies = [ "generational-box", "global-hotkey", "http-range", + "image", "infer", "jni 0.21.1", "lazy-js-bundle", diff --git a/_typos.toml b/_typos.toml index 81648ac4e..2c90856dc 100644 --- a/_typos.toml +++ b/_typos.toml @@ -14,4 +14,4 @@ unparented = "unparented" flate2="flate2" [files] -extend-exclude = ["notes/translations/*", "CHANGELOG.md", "*.js"] +extend-exclude = ["notes/translations/*", "CHANGELOG.md", "*.js", "packages/cli/src/build/windows.rs"] diff --git a/packages/cli/assets/icon.ico b/packages/cli/assets/icon.ico new file mode 100644 index 0000000000000000000000000000000000000000..243747b8d2d62222cfd708816cc4e4ac58ec7168 GIT binary patch literal 112500 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 literal 0 HcmV?d00001 diff --git a/packages/cli/schema.json b/packages/cli/schema.json index 73a61448f..aa147decf 100644 --- a/packages/cli/schema.json +++ b/packages/cli/schema.json @@ -541,6 +541,13 @@ "null" ] }, + "name": { + "description": "Display name of the application", + "type": [ + "string", + "null" + ] + }, "out_dir": { "type": [ "string", @@ -649,6 +656,12 @@ "type": "string" } }, + "file_version": { + "type": [ + "string", + "null" + ] + }, "icon": { "type": [ "array", @@ -680,6 +693,12 @@ } ] }, + "original_file_name": { + "type": [ + "string", + "null" + ] + }, "publisher": { "type": [ "string", @@ -701,6 +720,18 @@ "null" ] }, + "trademark": { + "type": [ + "string", + "null" + ] + }, + "version": { + "type": [ + "string", + "null" + ] + }, "windows": { "anyOf": [ { @@ -2864,6 +2895,13 @@ "WindowsSettings": { "type": "object", "properties": { + "add_toolkit_include": { + "description": "Pass the host Windows SDK's `um/` and `shared/` headers to `rc.exe` via `/I` flags. Only relevant when cross-compiling to MSVC from a non-Windows host where the SDK headers aren't on the default include path. Has no effect for the GNU (`windres`) toolchain.", + "type": [ + "boolean", + "null" + ] + }, "allow_downgrades": { "type": "boolean" }, @@ -2873,18 +2911,48 @@ "null" ] }, + "comments": { + "description": "Free-form developer comments embedded in the executable's VERSIONINFO, surfaced in Windows Explorer → Properties → Details → Comments.", + "type": [ + "string", + "null" + ] + }, "digest_algorithm": { "type": [ "string", "null" ] }, + "extra_rc": { + "description": "Raw `.rc` snippet appended verbatim to the generated resource file. Use this to add custom resources not covered by other settings — application manifests, custom string tables, accelerators, etc. Contents are not escaped.", + "type": [ + "string", + "null" + ] + }, "icon_path": { "type": [ "string", "null" ] }, + "internal_name": { + "description": "Internal name embedded in the executable's VERSIONINFO. Most apps leave this equal to the product name; override only if the binary is known internally by a different identifier (e.g. an original codename).", + "type": [ + "string", + "null" + ] + }, + "language": { + "description": "VERSIONINFO LANGID for the resource block. Hex form like `0x0409` (en-US) or `0x0407` (de-DE). Defaults to `0x0000` (neutral). See for the full table.", + "type": [ + "integer", + "null" + ], + "format": "uint16", + "minimum": 0.0 + }, "nsis": { "anyOf": [ { diff --git a/packages/cli/src/build/mod.rs b/packages/cli/src/build/mod.rs index a017995a8..3cf49a4ce 100644 --- a/packages/cli/src/build/mod.rs +++ b/packages/cli/src/build/mod.rs @@ -12,6 +12,7 @@ mod android; mod apple; mod web; +mod windows; mod assets; mod builder; diff --git a/packages/cli/src/build/request.rs b/packages/cli/src/build/request.rs index 9e3090618..652ffaa14 100644 --- a/packages/cli/src/build/request.rs +++ b/packages/cli/src/build/request.rs @@ -943,6 +943,15 @@ impl BuildRequest { AndroidTools::unpack_prebuilt_openssl()?; } + // Compile the Windows resource (.res) so it's ready to be linked in later + if matches!(self.triple.operating_system, OperatingSystem::Windows) { + if let Err(err) = self.write_winres() { + if self.using_dioxus_explicitly { + tracing::warn!("Application may not have an icon: {err}"); + } + } + } + Ok(()) } @@ -1767,6 +1776,11 @@ impl BuildRequest { cargo_args.push("-Clink-arg=-Wl,-rpath,$ORIGIN/../lib".to_string()); cargo_args.push("-Clink-arg=-Wl,-rpath,$ORIGIN".to_string()); } + OperatingSystem::Windows => { + if let Some((search_path, link_spec)) = self.winres_linker_args() { + cargo_args.extend(["-L".to_string(), search_path, "-l".to_string(), link_spec]); + } + } _ => {} } @@ -2505,7 +2519,7 @@ impl BuildRequest { /// target/dx/build/app/web/ /// target/dx/build/app/web/public/ /// target/dx/build/app/web/server.exe - fn platform_dir(&self) -> PathBuf { + pub(crate) fn platform_dir(&self) -> PathBuf { self.internal_out_dir() .join(&self.main_target) .join(if self.release { "release" } else { "debug" }) diff --git a/packages/cli/src/build/windows.rs b/packages/cli/src/build/windows.rs new file mode 100644 index 000000000..a327ba585 --- /dev/null +++ b/packages/cli/src/build/windows.rs @@ -0,0 +1,779 @@ +//! Modified version of or +//! +//! Rust Windows resource helper +//! +//! This crate implements a simple generator for Windows resource (.rc) files +//! for use with either Microsoft `rc.exe` resource compiler or with GNU `windres.exe` +//! +//! Maybe it could be replaced by in the future +//! if it's going to be used to compile resources as the name suggests instead of external tools + +use krates::semver::Version; + +use anyhow::{anyhow, Result}; +use std::collections::HashMap; +use std::env; +use std::fs::File; +use std::io; +use std::io::prelude::*; +use std::path::{Path, PathBuf}; +use std::process; +use target_lexicon::{Architecture, Environment, OperatingSystem, Triple, X86_32Architecture}; + +use crate::BuildRequest; + +const DEFAULT_ICON: &[u8] = include_bytes!("../../assets/icon.ico"); + +impl BuildRequest { + /// Per-build directory for the compiled Windows resource (`.rc` source, + /// the produced object/lib, and any default-icon copy). Lives under + /// [`Self::platform_dir`], which is already unique per + /// `(project, profile, bundle)` and is not cleared by `dx build`/`dx bundle`, + /// so multiple projects can't clobber each other. + pub(crate) fn winres_dir(&self) -> PathBuf { + self.platform_dir().join(".winres") + } + + /// Returns `(search_path, link_spec)` for the Windows resource library + /// produced by [`Self::write_winres`], if it has been built. This is a + /// pure lookup that derives the deterministic output paths and only + /// returns `Some` when the compiled lib is actually present on disk, so + /// it is safe to call from `cargo_build_arguments`. The pair is suitable + /// for `cargo rustc -L -l `. + pub(crate) fn winres_linker_args(&self) -> Option<(String, String)> { + if !matches!(self.triple.operating_system, OperatingSystem::Windows) { + return None; + } + + let winres_dir = self.winres_dir(); + + let (link_spec, lib_filename) = if matches!(self.triple.environment, Environment::Msvc) { + ("dylib=resource", "resource.lib") + } else { + ("static=resource", "libresource.a") + }; + + if !winres_dir.join(lib_filename).is_file() { + return None; + } + + Some(( + winres_dir.to_string_lossy().to_string(), + link_spec.to_string(), + )) + } + + // needs to only run when tomls are updated + pub(crate) fn write_winres(&self) -> Result<()> { + let bundle = &self.config.bundle; + let package = self.package(); + + let (version_str, version) = match bundle.version.as_ref() { + Some(v) => (v, VersionInfo::version_from_str(v)), + None => ( + &format!( + "{}.{}.{}", + package.version.major, package.version.minor, package.version.patch + ), + VersionInfo::version_from_krate(&package.version), + ), + }; + + let (file_version_str, file_version) = match bundle.file_version.as_ref() { + Some(v) => (v, VersionInfo::version_from_str(v)), + None => (version_str, version), + }; + + let productname = match self.config.application.name.as_ref() { + Some(n) => n, + None => &self.bundled_app_name(), + }; + + let binding = package.description.clone().unwrap_or_default(); + let description = match bundle.short_description.as_ref() { + Some(val) => val, + None => bundle.long_description.as_ref().unwrap_or(&binding), + }; + + let winres_dir = self.winres_dir(); + _ = std::fs::create_dir_all(&winres_dir); + + let mut winres = WindowsResource::new(version, file_version); + winres + .set(Properties::ProductVersion, version_str) + .set(Properties::FileVersion, file_version_str) + .set(Properties::ProductName, productname) + .set(Properties::FileDescription, description); + + if let Some(value) = &bundle.original_file_name { + winres.set(Properties::OriginalFilename, value); + } + + if let Some(value) = &bundle.copyright { + winres.set(Properties::Copyright, value); + } + if let Some(value) = &bundle.trademark { + winres.set(Properties::Trademark, value); + } + + if let Some(value) = &bundle.publisher { + winres.set(Properties::CompanyName, value); + } + + if let Some(value) = &bundle.category { + winres.set(Properties::Other("Category"), value); + } + + let windows = bundle.windows.as_ref(); + + if let Some(value) = windows.and_then(|w| w.comments.as_deref()) { + winres.set(Properties::Comments, value); + } + if let Some(value) = windows.and_then(|w| w.internal_name.as_deref()) { + winres.set(Properties::InternalName, value); + } + if let Some(language) = windows.and_then(|w| w.language) { + winres.set_language(language); + } + if windows.and_then(|w| w.add_toolkit_include).unwrap_or(false) { + winres.add_toolkit_include(true); + } + if let Some(value) = windows.and_then(|w| w.extra_rc.as_deref()) { + winres.append_rc_content(value); + } + + let mut has_default = false; + if let Some(path) = windows.and_then(|w| w.icon_path.as_ref()) { + winres.set_icon(path.clone()); + has_default = true; + } + + if let Some(icons) = bundle.icon.as_ref() { + for (id, icon) in icons.iter().enumerate() { + if icon.ends_with(".ico") { + let icon_path = self.canonicalize_icon_path(&PathBuf::from(icon))?; + if !has_default { + // first .ico file will be app icon + winres.set_icon(icon_path); + has_default = true; + } else { + // other .ico files can be accessed with their index as id + winres.set_icon_with_id(icon_path, id.to_string()); + }; + } + } + } + + if !has_default { + let default_icon = winres_dir.join("icon.ico"); + let mut file = File::create(&default_icon)?; + file.write_all(DEFAULT_ICON)?; + let icon = self.canonicalize_icon_path(&default_icon)?; + winres.set_icon(icon); + } + + winres.compile(&self.triple, &winres_dir)?; + Ok(()) + } + + fn canonicalize_icon_path(&self, icon_path: &PathBuf) -> Result { + if icon_path.is_absolute() && icon_path.is_file() { + return Ok(dunce::canonicalize(icon_path)?); + } + let crate_icon = self.crate_dir().join(icon_path); + if crate_icon.is_file() { + return Ok(dunce::canonicalize(crate_icon)?); + } + let workspace_icon = self.workspace_dir().join(icon_path); + if workspace_icon.is_file() { + return Ok(dunce::canonicalize(workspace_icon)?); + } + + Err(anyhow::anyhow!( + "Could not find icon from path {:?}", + icon_path + )) + } +} + +/// Values based on +/// use to_str() +#[allow(clippy::upper_case_acronyms)] +#[derive(PartialEq, Eq, Hash, Debug, Clone, Copy)] +pub enum IDI { + APPLICATION = 32512, + ERROR = 32513, + QUESTION = 32514, + WARNING = 32515, + INFORMATION = 32516, + WINLOGO = 32517, + SHIELD = 32518, +} + +impl std::fmt::Display for IDI { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{}", *self as u32) + } +} + +impl From for String { + fn from(e: IDI) -> Self { + e.to_string() + } +} + +/// Version info field names +#[allow(clippy::upper_case_acronyms)] +#[derive(PartialEq, Eq, Hash, Debug)] +pub enum VersionInfo { + /// The version value consists of four 16 bit words, e.g., + /// `MAJOR << 48 | MINOR << 32 | PATCH << 16 | RELEASE` + FILEVERSION, + /// The version value consists of four 16 bit words, e.g., + /// `MAJOR << 48 | MINOR << 32 | PATCH << 16 | RELEASE` + PRODUCTVERSION, + /// Should be Windows NT Win32, with value `0x40004` + FILEOS, + /// The value (for a rust compiler output) should be + /// 1 for a EXE and 2 for a DLL + FILETYPE, + /// Only for Windows drivers + FILESUBTYPE, + /// Bit mask for FILEFLAGS + FILEFLAGSMASK, + /// Only the bits set in FILEFLAGSMASK are read + FILEFLAGS, +} + +/// Common properties fields +/// +#[derive(PartialEq, Eq, Hash, Debug)] +pub enum Properties { + FileVersion, + FileDescription, + ProductVersion, + ProductName, + OriginalFilename, + Copyright, + Trademark, + CompanyName, + Comments, + InternalName, + /// Additionally there exists + /// `"PrivateBuild"`, `"SpecialBuild"` + /// which should only be set, when the `FILEFLAGS` property is set to + /// `VS_FF_PRIVATEBUILD(0x08)` or `VS_FF_SPECIALBUILD(0x20)` + /// + /// It is possible to use arbitrary field names but Windows Explorer and other + /// tools might not show them. + Other(&'static str), +} + +impl std::fmt::Display for Properties { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Properties::FileVersion => write!(f, "FileVersion"), + Properties::FileDescription => write!(f, "FileDescription"), + Properties::ProductVersion => write!(f, "ProductVersion"), + Properties::ProductName => write!(f, "ProductName"), + Properties::OriginalFilename => write!(f, "OriginalFilename"), + Properties::Copyright => write!(f, "LegalCopyright"), + Properties::Trademark => write!(f, "LegalTrademark"), + Properties::CompanyName => write!(f, "CompanyName"), + Properties::Comments => write!(f, "Comments"), + Properties::InternalName => write!(f, "InternalName"), + Properties::Other(other) => write!(f, "{}", other), + } + } +} + +impl VersionInfo { + /// Creates u64 version from string + fn version_from_str(version: &str) -> u64 { + let parts: Vec<&str> = version.split('.').collect(); + if parts.len() > 4 { + tracing::warn!("Version number had more than 4 parts. Ignoring the rest."); + } + let mut segments = [0u16; 4]; + for (i, part) in parts.iter().take(4).enumerate() { + match part.parse::() { + Ok(value) => segments[i] = value, + Err(e) => { + tracing::warn!( + "Could not parse segment {} '{}' as u16: {}. Defaulting to 0.", + i, + part, + e + ); + } + } + } + + ((segments[0] as u64) << 48) + | ((segments[1] as u64) << 32) + | ((segments[2] as u64) << 16) + | (segments[3] as u64) + } + + fn version_from_krate(v: &Version) -> u64 { + (v.major << 48) | (v.minor << 32) | (v.patch << 16) + } +} + +#[derive(Debug)] +struct Icon { + path: String, + name_id: String, +} + +#[derive(Debug)] +struct WindowsResource { + properties: HashMap, + version_info: HashMap, + icons: Vec, + language: u16, + add_toolkit_include: bool, + append_rc_content: String, +} + +impl WindowsResource { + /// Create a new resource with version info struct + /// + /// We initialize the resource file with values provided in Dioxus.toml + /// + fn new(version: u64, file_version: u64) -> Self { + let props: HashMap = HashMap::new(); + let mut ver: HashMap = HashMap::new(); + + ver.insert(VersionInfo::FILEVERSION, file_version); + ver.insert(VersionInfo::PRODUCTVERSION, version); + ver.insert(VersionInfo::FILEOS, 0x00040004); // VOS_NT_WINDOWS32 (0x40004) + ver.insert(VersionInfo::FILETYPE, 1); // VFT_APP (0x1) + ver.insert(VersionInfo::FILESUBTYPE, 0); // VFT2_UNKNOWN (0x0) + ver.insert(VersionInfo::FILEFLAGSMASK, 0x3F); // VS_FFI_FILEFLAGSMASK (0x3F) + ver.insert(VersionInfo::FILEFLAGS, 0); // 0x0 + + WindowsResource { + properties: props, + version_info: ver, + icons: Vec::new(), + language: 0, + add_toolkit_include: false, + append_rc_content: String::new(), + } + } + + /// Set string properties of the version info struct. + /// + /// See [`Properties`] for valid values + fn set(&mut self, name: Properties, value: &str) -> &mut Self { + self.properties.insert(name.to_string(), value.to_string()); + self + } + + /// Set the user interface language of the file + /// + /// For possible values look at the `winapi::um::winnt` constants, specifically those + /// starting with `LANG_` and `SUBLANG_` or at + /// + /// [`MAKELANGID`]: https://docs.rs/winapi/0.3/x86_64-pc-windows-msvc/winapi/um/winnt/fn.MAKELANGID.html + /// [`winapi::um::winnt`]: https://docs.rs/winapi/0.3/x86_64-pc-windows-msvc/winapi/um/winnt/index.html#constants + /// + /// # Table + /// Sometimes it is just simpler to specify the numeric constant directly + /// (That is what most `.rc` files do). + /// For possible values take a look at the MSDN page for resource files; + /// we only listed some values here. + /// + /// | Language | Value | + /// |---------------------|----------| + /// | Neutral | `0x0000` | + /// | English | `0x0009` | + /// | English (US) | `0x0409` | + /// | English (GB) | `0x0809` | + /// | German | `0x0407` | + /// | German (AT) | `0x0c07` | + /// | French | `0x000c` | + /// | French (FR) | `0x040c` | + /// | Catalan | `0x0003` | + /// | Basque | `0x042d` | + /// | Breton | `0x007e` | + /// | Scottish Gaelic | `0x0091` | + /// | Romansch | `0x0017` | + fn set_language(&mut self, language: u16) -> &mut Self { + self.language = language; + self + } + + /// Add an icon with nameID `1`. + /// + /// This icon need to be in `ico` format. The filename can be absolute + /// or relative to the projects root. + /// + /// Equivalent ```to set_icon_with_id(path, IDI::APPLICATION)```. + fn set_icon(&mut self, path: PathBuf) -> &mut Self { + self.set_icon_with_id(path, IDI::APPLICATION) + } + + /// Add an icon with the specified name ID. + /// + /// This icon need to be in `ico` format. The path can be absolute or + /// relative to the projects root. + /// + /// ## Name ID and Icon Loading + /// + /// The name ID can be (the string representation of) a 16-bit unsigned + /// integer, or some other string. + /// + /// You should not add multiple icons with the same name ID. It will result + /// in a build failure. + /// + /// When the name ID is an integer, the icon can be loaded at runtime with + /// + /// ```ignore + /// LoadIconW(h_instance, MAKEINTRESOURCEW(name_id_as_integer)) + /// ``` + /// + /// Otherwise, it can be loaded with + /// + /// ```ignore + /// LoadIconW(h_instance, name_id_as_wide_c_str_as_ptr) + /// ``` + /// + /// Where `h_instance` is the module handle of the current executable + /// ([`GetModuleHandleW`](https://docs.rs/winapi/0.3.8/winapi/um/libloaderapi/fn.GetModuleHandleW.html)`(null())`), + /// [`LoadIconW`](https://docs.rs/winapi/0.3.8/winapi/um/winuser/fn.LoadIconW.html) + /// and + /// [`MAKEINTRESOURCEW`](https://docs.rs/winapi/0.3.8/winapi/um/winuser/fn.MAKEINTRESOURCEW.html) + /// are defined in winapi. + /// + /// ## Multiple Icons, Which One is Application Icon? + /// + /// When you have multiple icons, it's a bit complicated which one will be + /// chosen as the application icon: + /// . + /// + /// To keep things simple, we recommend you use only 16-bit unsigned integer + /// name IDs, and add the application icon first with the lowest id: + /// + /// see [`IDI`] for special icons ids + fn set_icon_with_id(&mut self, path: PathBuf, name_id: impl Into) -> &mut Self { + self.icons.push(Icon { + path: path.to_string_lossy().to_string(), + name_id: name_id.into(), + }); + self + } + + /// Set the path to the ar executable. + fn add_toolkit_include(&mut self, add: bool) -> &mut Self { + self.add_toolkit_include = add; + self + } + + /// Write a resource file with the set values + fn write_resource_file(&self, path: &Path) -> Result { + let path = path.join("resource.rc"); + let mut f = File::create(&path)?; + + // use UTF8 as an encoding + // this makes it easier since in rust all string are UTF8 + writeln!(f, "#pragma code_page(65001)")?; + writeln!(f, "1 VERSIONINFO")?; + for (k, v) in self.version_info.iter() { + match *k { + VersionInfo::FILEVERSION | VersionInfo::PRODUCTVERSION => writeln!( + f, + "{:?} {}, {}, {}, {}", + k, + (*v >> 48) as u16, + (*v >> 32) as u16, + (*v >> 16) as u16, + *v as u16 + )?, + _ => writeln!(f, "{:?} {:#x}", k, v)?, + }; + } + writeln!(f, "{{\nBLOCK \"StringFileInfo\"")?; + writeln!(f, "{{\nBLOCK \"{:04x}04b0\"\n{{", self.language)?; + for (k, v) in self.properties.iter() { + if !v.is_empty() { + writeln!( + f, + "VALUE \"{}\", \"{}\"", + escape_string_windows(k), + escape_string_windows(v) + )?; + } + } + writeln!(f, "}}\n}}")?; + + writeln!(f, "BLOCK \"VarFileInfo\" {{")?; + writeln!(f, "VALUE \"Translation\", {:#x}, 0x04b0", self.language)?; + writeln!(f, "}}\n}}")?; + for icon in &self.icons { + writeln!( + f, + "{} ICON \"{}\"", + escape_string_windows(&icon.name_id), + escape_string_windows(&icon.path) + )?; + } + + writeln!(f, "{}", self.append_rc_content)?; + Ok(path) + } + + /// Append an additional snippet to the generated rc file. + fn append_rc_content(&mut self, content: &str) -> &mut Self { + if !(self.append_rc_content.ends_with('\n') || self.append_rc_content.is_empty()) { + self.append_rc_content.push('\n'); + } + self.append_rc_content.push_str(content); + self + } + + /// Run the resource compiler + /// + /// This function generates a resource file from the settings or + /// uses an existing resource file and passes it to the resource compiler + /// of your toolkit. + fn compile(&mut self, target: &Triple, output_dir: &Path) -> Result<()> { + if matches!(target.environment, Environment::Msvc) { + tracing::debug!("Compiling Windows resource file with msvc toolkit"); + self.compile_with_toolkit_msvc(target, output_dir) + } else if target.environment.to_string().contains("gnu") { + tracing::debug!("Compiling Windows resource file with gnu toolkit"); + self.compile_with_toolkit_gnu(target, output_dir) + } else { + Err(anyhow!( + "Can only compile resource file when target_env is 'gnu' or 'msvc'", + )) + } + } + + fn compile_with_toolkit_gnu(&mut self, target: &Triple, output_dir: &Path) -> Result<()> { + let toolkit_path = + if env::var_os("HOST").is_some_and(|v| v.to_string_lossy().contains("windows")) { + PathBuf::from("\\") + } else { + PathBuf::from("/") + }; + + let prefix = if env::var_os("HOST") + .zip(env::var_os("TARGET")) + .is_some_and(|(h, t)| h != t) + { + match (target.architecture, target.environment) { + // Standard MinGW-w64 targets + (Architecture::X86_64, Environment::Gnu) => "x86_64-w64-mingw32-", + (Architecture::X86_32(X86_32Architecture::I686), Environment::Gnu) => { + "i686-w64-mingw32-" + } + (Architecture::X86_32(X86_32Architecture::I586), Environment::Gnu) => { + "i586-w64-mingw32-" + } + // MinGW supports ARM64 only with an LLVM-based toolchain + // (x86 users might also be using LLVM, but we can't tell that from the Rust target...) + (Architecture::Aarch64(_), Environment::Gnu) => "llvm-", + + // LLVM-based MinGW targets (gnullvm) always use llvm- prefix + (_, Environment::GnuLlvm) => "llvm-", + // fail safe + _ => { + tracing::warn!("Unknown Windows target used for cross-compilation - invoking unprefixed windres"); + "" + } + } + } else { + "" + }; + + // this could probably be done with variables in Dioxus.toml instead of env::var + let windres_path = if let Ok(windres) = env::var("WINDRES") { + windres + } else { + format!("{}windres", prefix) + }; + let ar_path = if let Ok(ar) = env::var("AR") { + ar + } else { + format!("{}ar", prefix) + }; + + let rc_file = self.write_resource_file(output_dir)?; + + tracing::debug!("Input file: '{}'", rc_file.display()); + let output = output_dir.join("resource.o"); + tracing::debug!("Output object file: '{}'", output.display()); + + tracing::debug!("Selected toolkit path: '{}'", &toolkit_path.display()); + tracing::debug!("Selected windres path: '{:?}'", &windres_path); + tracing::debug!("Selected ar path: '{:?}'", &ar_path); + + let status = process::Command::new(&windres_path) + .current_dir(&toolkit_path) + .arg(format!("{}", rc_file.display())) + .arg(format!("{}", output.display())) + .output()?; + + if !status.status.success() { + return Err(anyhow!("Compiling resource file {:?}", &status.stderr)); + } + + let libname = output_dir.join("libresource.a"); + tracing::debug!("Output lib file: '{}'", output.display()); + let status = process::Command::new(&ar_path) + .current_dir(&toolkit_path) + .arg("rsc") + .arg(format!("{}", libname.display())) + .arg(format!("{}", output.display())) + .output()?; + + if !status.status.success() { + return Err(anyhow!( + "Creating static library for resource file {:?}", + &status.stderr + )); + } + + Ok(()) + } + + fn compile_with_toolkit_msvc(&mut self, target: &Triple, output_dir: &Path) -> Result<()> { + // The path to this could also be provided via Dioxus.toml if someone has the exe in other places + let toolkit = get_sdk(matches!(target.architecture, Architecture::X86_64))?; + + let rc_file = self.write_resource_file(output_dir)?; + + tracing::debug!("Selected toolkit path: '{}'", toolkit.display()); + tracing::debug!("Input file: '{}'", rc_file.display()); + let output = output_dir.join("resource.lib"); + tracing::debug!("Output file: '{}'", output.display()); + let mut command = process::Command::new(&toolkit); + + if self.add_toolkit_include { + let root = win_sdk_include_root(&toolkit); + tracing::debug!("Adding toolkit include: {}", root.display()); + command.arg(format!("/I{}", root.join("um").display())); + command.arg(format!("/I{}", root.join("shared").display())); + } + + let status = command + .arg(format!("/fo{}", output.display())) + .arg(format!("{}", rc_file.display())) + .output()?; + + if !status.status.success() { + return Err(anyhow!("Compiling resource file {:?}", &status.stderr)); + } + + Ok(()) + } +} + +/// Find a Windows SDK +fn get_sdk(is_x64: bool) -> io::Result { + // use the reg command, so we don't need a winapi dependency + let output = process::Command::new("reg") + .arg("query") + .arg(r"HKLM\SOFTWARE\Microsoft\Windows Kits\Installed Roots") + .arg("/reg:32") + .output()?; + + if !output.status.success() { + return Err(io::Error::other(format!( + "Querying the registry failed with error message:\n{}", + String::from_utf8(output.stderr).map_err(|e| io::Error::other(e.to_string()))? + ))); + } + + let lines = String::from_utf8(output.stdout).map_err(|e| io::Error::other(e.to_string()))?; + let mut lines: Vec<&str> = lines.lines().collect(); + lines.reverse(); + for line in lines { + if line.trim().starts_with("KitsRoot") { + let kit: String = line + .chars() + .skip(line.find("REG_SZ").unwrap() + 6) + .skip_while(|c| c.is_whitespace()) + .collect(); + + let p = PathBuf::from(&kit); + let rc = if is_x64 { + p.join(r"bin\x64\rc.exe") + } else { + p.join(r"bin\x86\rc.exe") + }; + + if rc.exists() { + return Ok(rc); + } + + if let Ok(bin) = p.join("bin").read_dir() { + for e in bin.filter_map(|e| e.ok()) { + let p = if is_x64 { + e.path().join(r"x64\rc.exe") + } else { + e.path().join(r"x86\rc.exe") + }; + if p.exists() { + return Ok(p); + } + } + } + } + } + Err(io::Error::other("Can not find Windows SDK")) +} + +/// Escapes `string` so it can be safely embedded between the double-quotes of +/// a Windows resource script (`.rc`) string literal, as consumed by +/// `windres` / `rc.exe`. +/// +/// Used by [`WindowsResource::write_resource_file`] for every quoted value we +/// emit — `VERSIONINFO` `VALUE` entries (`ProductName`, `FileDescription`, +/// etc.) and the path / name-id of each `ICON` resource. Without this, an +/// unescaped `"` or `\` in (for example) a user-supplied `productName` or +/// icon path would produce a malformed `.rc` and a confusing compiler error. +/// +/// The RC string-literal grammar is a hybrid: +/// - `"` is doubled (`""`) — RC's own quoting convention, *not* `\"`. +/// - Backslash escapes follow C conventions: `\\`, `\n`, `\t`, `\r`, `\'`. +/// +/// See the RC reference: +/// +fn escape_string_windows(string: &str) -> String { + let mut escaped = String::new(); + for chr in string.chars() { + match chr { + '"' => escaped.push_str("\"\""), + '\'' => escaped.push_str("\\'"), + '\\' => escaped.push_str("\\\\"), + '\n' => escaped.push_str("\\n"), + '\t' => escaped.push_str("\\t"), + '\r' => escaped.push_str("\\r"), + _ => escaped.push(chr), + }; + } + escaped +} + +fn win_sdk_include_root(path: &Path) -> PathBuf { + let mut tools_path = PathBuf::new(); + let mut iter = path.iter(); + while let Some(p) = iter.next() { + if p == "bin" { + let version = iter.next().unwrap(); + tools_path.push("Include"); + if version.to_string_lossy().starts_with("10.") { + tools_path.push(version); + } + break; + } else { + tools_path.push(p); + } + } + + tools_path +} diff --git a/packages/cli/src/cli/bundle.rs b/packages/cli/src/cli/bundle.rs index a37fa5be2..0e64881ea 100644 --- a/packages/cli/src/cli/bundle.rs +++ b/packages/cli/src/cli/bundle.rs @@ -4,7 +4,7 @@ use crate::{ use anyhow::Context; use path_absolutize::Absolutize; use std::path::PathBuf; -use target_lexicon::Triple; +use target_lexicon::{OperatingSystem, Triple}; use super::*; @@ -158,7 +158,8 @@ impl Bundle { _ = std::fs::remove_dir_all(krate.bundle_dir(build.bundle)); let mut name: PathBuf = krate.executable_name().into(); - if cfg!(windows) { + + if build.triple.operating_system == OperatingSystem::Windows { name.set_extension("exe"); } std::fs::create_dir_all(krate.bundle_dir(build.bundle)) diff --git a/packages/cli/src/config/app.rs b/packages/cli/src/config/app.rs index b7248fda0..9cd39fd8a 100644 --- a/packages/cli/src/config/app.rs +++ b/packages/cli/src/config/app.rs @@ -4,6 +4,10 @@ use std::path::PathBuf; #[derive(Default, Debug, Clone, Serialize, Deserialize, JsonSchema)] pub(crate) struct ApplicationConfig { + /// Display name of the application + #[serde(default)] + pub(crate) name: Option, + /// The path where global assets will be added when components are added with `dx components add` #[serde(default)] pub(crate) asset_dir: Option, diff --git a/packages/cli/src/config/bundle.rs b/packages/cli/src/config/bundle.rs index 3bff37ea4..f58f279e4 100644 --- a/packages/cli/src/config/bundle.rs +++ b/packages/cli/src/config/bundle.rs @@ -31,6 +31,14 @@ pub(crate) struct BundleConfig { pub(crate) windows: Option, #[serde(default)] pub(crate) android: Option, + #[serde(default)] + pub(crate) version: Option, + #[serde(default)] + pub(crate) file_version: Option, + #[serde(default)] + pub(crate) original_file_name: Option, + #[serde(default)] + pub(crate) trademark: Option, } #[derive(Debug, Clone, Serialize, Deserialize, Default, JsonSchema)] @@ -225,6 +233,38 @@ pub(crate) struct WindowsSettings { /// need to use another tool like `osslsigncode`. #[serde(default)] pub sign_command: Option, + + /// Free-form developer comments embedded in the executable's VERSIONINFO, + /// surfaced in Windows Explorer → Properties → Details → Comments. + #[serde(default)] + pub(crate) comments: Option, + + /// Internal name embedded in the executable's VERSIONINFO. Most apps leave + /// this equal to the product name; override only if the binary is known + /// internally by a different identifier (e.g. an original codename). + #[serde(default)] + pub(crate) internal_name: Option, + + /// VERSIONINFO LANGID for the resource block. Hex form like `0x0409` + /// (en-US) or `0x0407` (de-DE). Defaults to `0x0000` (neutral). See + /// + /// for the full table. + #[serde(default)] + pub(crate) language: Option, + + /// Pass the host Windows SDK's `um/` and `shared/` headers to `rc.exe` via + /// `/I` flags. Only relevant when cross-compiling to MSVC from a + /// non-Windows host where the SDK headers aren't on the default include + /// path. Has no effect for the GNU (`windres`) toolchain. + #[serde(default)] + pub(crate) add_toolkit_include: Option, + + /// Raw `.rc` snippet appended verbatim to the generated resource file. + /// Use this to add custom resources not covered by other settings — + /// application manifests, custom string tables, accelerators, etc. + /// Contents are not escaped. + #[serde(default)] + pub(crate) extra_rc: Option, } #[derive(Default, Debug, Clone, Serialize, Deserialize, JsonSchema)] diff --git a/packages/cli/src/config/dioxus_config.rs b/packages/cli/src/config/dioxus_config.rs index eaf1d8bc4..33f6d13e6 100644 --- a/packages/cli/src/config/dioxus_config.rs +++ b/packages/cli/src/config/dioxus_config.rs @@ -102,6 +102,7 @@ impl Default for DioxusConfig { fn default() -> Self { Self { application: ApplicationConfig { + name: None, asset_dir: None, out_dir: None, public_dir: Some("public".into()), diff --git a/packages/desktop/Cargo.toml b/packages/desktop/Cargo.toml index 5aa36ab19..5385075e9 100644 --- a/packages/desktop/Cargo.toml +++ b/packages/desktop/Cargo.toml @@ -21,6 +21,8 @@ dioxus-asset-resolver = { workspace = true, features = ["native"] } generational-box = { workspace = true } dioxus-devtools = { workspace = true, optional = true } +anyhow = { workspace = true } +image = { workspace = true } serde = { workspace = true } serde_json = { workspace = true } thiserror = { workspace = true } diff --git a/packages/desktop/src/default_icon.rs b/packages/desktop/src/default_icon.rs new file mode 100644 index 000000000..77941cff9 --- /dev/null +++ b/packages/desktop/src/default_icon.rs @@ -0,0 +1,187 @@ +use anyhow::Result; +use image::load_from_memory; +use image::GenericImageView; +use image::ImageReader; +use std::path::Path; + +/// Pre-decoded RGBA bytes of the bundled fallback icon. +const FALLBACK_ICON_RGBA: &[u8] = include_bytes!("./assets/default_icon.bin"); +const FALLBACK_ICON_WIDTH: u32 = 460; +const FALLBACK_ICON_HEIGHT: u32 = 460; + +/// Trait that creates icons for various types +pub trait DioxusIconTrait { + fn get_icon() -> Result + where + Self: Sized; + fn from_memory(value: &[u8]) -> Result + where + Self: Sized; + fn path>(path: P, size: Option<(u32, u32)>) -> Result + where + Self: Sized; +} + +#[cfg(any(target_os = "linux", target_os = "macos", target_os = "windows"))] +use crate::trayicon::DioxusTrayIcon; + +fn load_image_from_memory(value: &[u8]) -> Result<(Vec, u32, u32)> { + let img = load_from_memory(value)?; + let rgba = img.to_rgba8(); + let (width, height) = img.dimensions(); + Ok((rgba.to_vec(), width, height)) +} + +fn load_image_from_path>(path: P) -> Result<(Vec, u32, u32)> { + let img = ImageReader::open(path)?.decode()?; + let rgba = img.to_rgba8(); + let (width, height) = img.dimensions(); + Ok((rgba.to_vec(), width, height)) +} + +#[cfg(any(target_os = "linux", target_os = "macos", target_os = "windows"))] +impl DioxusIconTrait for DioxusTrayIcon { + fn get_icon() -> Result + where + Self: Sized, + { + #[cfg(target_os = "windows")] + if let Ok(icon) = DioxusTrayIcon::from_resource(32512, None) { + return Ok(icon); + } + DioxusTrayIcon::from_rgba( + FALLBACK_ICON_RGBA.to_vec(), + FALLBACK_ICON_WIDTH, + FALLBACK_ICON_HEIGHT, + ) + .map_err(Into::into) + } + + fn from_memory(value: &[u8]) -> Result + where + Self: Sized, + { + let (icon, width, height) = load_image_from_memory(value)?; + DioxusTrayIcon::from_rgba(icon, width, height).map_err(Into::into) + } + + fn path>(path: P, size: Option<(u32, u32)>) -> Result + where + Self: Sized, + { + let (img, width, height) = load_image_from_path(path)?; + if let Some((width, height)) = size { + Ok(DioxusTrayIcon::from_rgba(img, width, height)?) + } else { + Ok(DioxusTrayIcon::from_rgba(img, width, height)?) + } + } +} + +#[cfg(not(any(target_os = "ios", target_os = "android")))] +use crate::menubar::DioxusMenuIcon; + +#[cfg(not(any(target_os = "ios", target_os = "android")))] +impl DioxusIconTrait for DioxusMenuIcon { + fn get_icon() -> Result + where + Self: Sized, + { + #[cfg(target_os = "windows")] + if let Ok(icon) = DioxusMenuIcon::from_resource(32512, None) { + return Ok(icon); + } + DioxusMenuIcon::from_rgba( + FALLBACK_ICON_RGBA.to_vec(), + FALLBACK_ICON_WIDTH, + FALLBACK_ICON_HEIGHT, + ) + .map_err(Into::into) + } + + fn from_memory(value: &[u8]) -> Result + where + Self: Sized, + { + let (icon, width, height) = load_image_from_memory(value)?; + DioxusMenuIcon::from_rgba(icon, width, height).map_err(Into::into) + } + + fn path>(path: P, size: Option<(u32, u32)>) -> Result + where + Self: Sized, + { + let (img, width, height) = load_image_from_path(path)?; + if let Some((width, height)) = size { + Ok(DioxusMenuIcon::from_rgba(img, width, height)?) + } else { + Ok(DioxusMenuIcon::from_rgba(img, width, height)?) + } + } +} + +use tao::window::Icon; + +#[cfg(target_os = "windows")] +use tao::platform::windows::IconExtWindows; + +impl DioxusIconTrait for Icon { + fn get_icon() -> Result + where + Self: Sized, + { + #[cfg(target_os = "windows")] + if let Ok(icon) = Icon::from_resource(32512, None) { + return Ok(icon); + } + Icon::from_rgba( + FALLBACK_ICON_RGBA.to_vec(), + FALLBACK_ICON_WIDTH, + FALLBACK_ICON_HEIGHT, + ) + .map_err(Into::into) + } + + fn from_memory(value: &[u8]) -> Result + where + Self: Sized, + { + let (icon, width, height) = load_image_from_memory(value)?; + Icon::from_rgba(icon, width, height).map_err(Into::into) + } + + fn path>(path: P, size: Option<(u32, u32)>) -> Result + where + Self: Sized, + { + let (img, width, height) = load_image_from_path(path)?; + if let Some((width, height)) = size { + Ok(Icon::from_rgba(img, width, height)?) + } else { + Ok(Icon::from_rgba(img, width, height)?) + } + } +} + +/// Provides the default icon of the app. +/// +/// On Windows this prefers the icon embedded as resource id `IDI::APPLICATION` +/// (32512) by `dx`'s bundler, falling back to a generic Dioxus icon when the +/// resource is missing (e.g. when running with plain `cargo run`). On all +/// other platforms the bundled fallback icon is returned directly. +pub fn default_icon() -> Result { + T::get_icon() +} + +/// Helper function to load image from include_bytes!("image.png") +pub fn icon_from_memory(value: &[u8]) -> Result { + T::from_memory(value) +} + +/// Helper function to load image from path +pub fn icon_from_path>( + path: P, + size: Option<(u32, u32)>, +) -> Result { + T::path(path, size) +} diff --git a/packages/desktop/src/lib.rs b/packages/desktop/src/lib.rs index 22b3cb6c8..8d91e5cb7 100644 --- a/packages/desktop/src/lib.rs +++ b/packages/desktop/src/lib.rs @@ -9,6 +9,7 @@ mod android_sync_lock; mod app; mod assets; mod config; +mod default_icon; mod desktop_context; mod document; mod edits; @@ -50,6 +51,7 @@ pub mod trayicon; // Public exports pub use assets::AssetRequest; pub use config::{Config, WindowCloseBehaviour}; +pub use default_icon::{default_icon, icon_from_memory, icon_from_path}; pub use desktop_context::{ window, DesktopContext, DesktopService, PendingDesktopContext, WeakDesktopContext, }; diff --git a/packages/desktop/src/menubar.rs b/packages/desktop/src/menubar.rs index b63d07d17..0817bd6f4 100644 --- a/packages/desktop/src/menubar.rs +++ b/packages/desktop/src/menubar.rs @@ -5,6 +5,11 @@ pub type DioxusMenu = muda::Menu; #[cfg(any(target_os = "ios", target_os = "android"))] pub type DioxusMenu = (); +#[cfg(not(any(target_os = "ios", target_os = "android")))] +pub type DioxusMenuIcon = muda::Icon; +#[cfg(any(target_os = "ios", target_os = "android"))] +pub type DioxusMenuIcon = (); + /// Initializes the menu bar for the window. #[allow(unused)] pub fn init_menu_bar(menu: &DioxusMenu, window: &Window) { diff --git a/packages/desktop/src/trayicon.rs b/packages/desktop/src/trayicon.rs index 8cc65f7d2..41170a98e 100644 --- a/packages/desktop/src/trayicon.rs +++ b/packages/desktop/src/trayicon.rs @@ -28,20 +28,21 @@ pub type DioxusTray = (); pub fn init_tray_icon(menu: DioxusTrayMenu, icon: Option) -> DioxusTray { #[cfg(any(target_os = "windows", target_os = "linux", target_os = "macos"))] { - let builder = tray_icon::TrayIconBuilder::new() - .with_menu(Box::new(menu)) - .with_menu_on_left_click(false) - .with_icon(match icon { - Some(value) => value, - None => tray_icon::Icon::from_rgba( - include_bytes!("./assets/default_icon.bin").to_vec(), - 460, - 460, - ) - .expect("image parse failed"), - }); + let icon = icon.map(Ok).unwrap_or_else(crate::default_icon); - provide_context(builder.build().expect("tray icon builder failed")) + let tray = tray_icon::TrayIconBuilder::new() + .with_menu(Box::new(menu)) + .with_menu_on_left_click(false); + + let tray = match icon { + Ok(icon) => tray.with_icon(icon), + Err(err) => { + tracing::trace!("No tray icon: {:?}", err); + tray + } + }; + + provide_context(tray.build().expect("tray icon builder failed")) } } diff --git a/packages/desktop/src/webview.rs b/packages/desktop/src/webview.rs index e1ec136d2..9710721fc 100644 --- a/packages/desktop/src/webview.rs +++ b/packages/desktop/src/webview.rs @@ -234,14 +234,7 @@ impl WebviewInstance { // We assume that if the icon is None in cfg, then the user just didnt set it if cfg.window.window.window_icon.is_none() { - window = window.with_window_icon(Some( - tao::window::Icon::from_rgba( - include_bytes!("./assets/default_icon.bin").to_vec(), - 460, - 460, - ) - .expect("image parse failed"), - )); + window = window.with_window_icon(crate::default_icon().ok()); } let window = Arc::new(window.build(&shared.target).unwrap());