diff --git a/Cargo.lock b/Cargo.lock index c801749faa..4ed6d040f7 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -5613,6 +5613,7 @@ version = "0.7.0" name = "dioxus-desktop" version = "0.7.0" dependencies = [ + "anyhow", "async-trait", "base64 0.22.1", "bytes", @@ -5637,6 +5638,7 @@ dependencies = [ "generational-box", "global-hotkey", "http-range", + "image", "infer", "jni 0.21.1", "lazy-js-bundle", diff --git a/Cargo.toml b/Cargo.toml index aefefaccd5..334678f3f5 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -357,6 +357,7 @@ pin-project = { version = "1.1.10" } postcard = { version = "1.1.3", default-features = false } serde_urlencoded = "0.7" form_urlencoded = "1.2.1" +image = "0.25.6" # desktop wry = { version = "0.53.5", default-features = false } diff --git a/_typos.toml b/_typos.toml index bf85d1085e..6172ffc367 100644 --- a/_typos.toml +++ b/_typos.toml @@ -12,4 +12,4 @@ udid = "udid" unparented = "unparented" [files] -extend-exclude = ["translations/*", "CHANGELOG.md", "*.js"] +extend-exclude = ["translations/*", "CHANGELOG.md", "*.js", "winres.rs"] diff --git a/packages/cli-config/src/lib.rs b/packages/cli-config/src/lib.rs index da3e77b44d..676c509a43 100644 --- a/packages/cli-config/src/lib.rs +++ b/packages/cli-config/src/lib.rs @@ -65,7 +65,7 @@ pub const ALWAYS_ON_TOP_ENV: &str = "DIOXUS_ALWAYS_ON_TOP"; pub const ASSET_ROOT_ENV: &str = "DIOXUS_ASSET_ROOT"; pub const APP_TITLE_ENV: &str = "DIOXUS_APP_TITLE"; pub const PRODUCT_NAME_ENV: &str = "DIOXUS_PRODUCT_NAME"; - +pub const APP_ICON_ENV: &str = "DIOXUS_APP_ICON"; #[deprecated(since = "0.6.0", note = "The CLI currently does not set this.")] #[doc(hidden)] pub const OUT_DIR: &str = "DIOXUS_OUT_DIR"; diff --git a/packages/cli/assets/icon.ico b/packages/cli/assets/icon.ico new file mode 100644 index 0000000000..243747b8d2 Binary files /dev/null and b/packages/cli/assets/icon.ico differ diff --git a/packages/cli/src/build/mod.rs b/packages/cli/src/build/mod.rs index c1cf1fcfa5..554d7a7abb 100644 --- a/packages/cli/src/build/mod.rs +++ b/packages/cli/src/build/mod.rs @@ -16,7 +16,7 @@ mod patch; mod pre_render; mod request; mod tools; - +pub(crate) mod winres; pub(crate) use assets::*; pub(crate) use builder::*; pub(crate) use context::*; diff --git a/packages/cli/src/build/request.rs b/packages/cli/src/build/request.rs index 9064130b34..91d7907199 100644 --- a/packages/cli/src/build/request.rs +++ b/packages/cli/src/build/request.rs @@ -329,7 +329,7 @@ use cargo_metadata::diagnostic::Diagnostic; use cargo_toml::{Profile, Profiles, StripSetting}; use depinfo::RustcDepInfo; use dioxus_cli_config::{format_base_path_meta_element, PRODUCT_NAME_ENV}; -use dioxus_cli_config::{APP_TITLE_ENV, ASSET_ROOT_ENV}; +use dioxus_cli_config::{APP_ICON_ENV, APP_TITLE_ENV, ASSET_ROOT_ENV}; use dioxus_cli_opt::{process_file_to, AssetManifest}; use itertools::Itertools; use krates::{cm::TargetKind, NodeId}; @@ -2695,6 +2695,12 @@ 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 self.release { + let res = self.write_winres().expect("Winres file"); + cargo_args.extend(["-L".to_string(), res.path, "-l".to_string(), res.lib]); + } + } _ => {} } @@ -2783,6 +2789,35 @@ impl BuildRequest { cargo_args } + fn absolute_icon_path(&self, path: &str) -> Result { + let icon_path = PathBuf::from(path); + let workspace_icon = self.workspace_dir().join(path); + let crate_icon = self.crate_dir().join(path); + + if icon_path.is_absolute() && icon_path.is_file() { + Ok(dunce::canonicalize(icon_path)?) + } else if workspace_icon.is_file() { + Ok(dunce::canonicalize(workspace_icon)?) + } else if crate_icon.is_file() { + Ok(dunce::canonicalize(crate_icon)?) + } else { + Err(anyhow::anyhow!("Could not find icon from path {}", path)) + } + } + + fn app_icon_path(&self) -> Result { + match self + .config + .bundle + .icon + .as_ref() + .and_then(|v| v.iter().find(|s| !s.to_lowercase().ends_with(".svg"))) + { + Some(value) => self.absolute_icon_path(value), + None => Err(anyhow::anyhow!("No icon set in Dioxus.toml")), + } + } + pub(crate) fn cargo_build_env_vars( &self, build_mode: &BuildMode, @@ -2807,6 +2842,15 @@ impl BuildRequest { env_vars.push((PRODUCT_NAME_ENV.into(), self.bundled_app_name().into())); } + if self.triple.operating_system == OperatingSystem::Windows && !self.release + || self.triple.operating_system != OperatingSystem::Windows + { + match self.app_icon_path() { + Ok(icon_path) => env_vars.push((APP_ICON_ENV.into(), icon_path.into())), + Err(err) => tracing::warn!("{:?}", err), + } + } + // Assemble the rustflags by peering into the `.cargo/config.toml` file let rust_flags = self.rustflags.clone(); @@ -5306,6 +5350,104 @@ __wbg_init({{module_or_path: "/{}/{wasm_path}"}}).then((wasm) => {{ Ok(()) } + // needs to only run when tomls are updated + fn write_winres(&self) -> Result { + use crate::winres::*; + 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), + }; + + // platform dir gets cleared on bundle + let mut output_dir = self.platform_dir(); + output_dir.pop(); + output_dir.push("winres"); + + std::fs::create_dir_all(&output_dir)?; + + let mut winres = WindowsResource::new(); + winres + .set_link(false) + .set_output_directory(output_dir.to_str().unwrap()) + .set_assets_path(Some(self.asset_dir().to_string_lossy().to_string())) + .set_version_info(VersionInfo::PRODUCTVERSION, version) + .set_version_info(VersionInfo::FILEVERSION, file_version) + .set("ProductVersion", version_str) + .set("FileVersion", file_version_str) + .set("ProductName", productname) + .set("FileDescription", description); + + if let Some(value) = &bundle.original_file_name { + winres.set("OriginalFilename", value); + } + + if let Some(value) = &bundle.copyright { + winres.set("LegalCopyright", value); + } + if let Some(value) = &bundle.trademark { + winres.set("LegalTrademark", value); + } + + if let Some(value) = &bundle.publisher { + winres.set("CompanyName", value); + } + + if let Some(value) = &bundle.category { + winres.set("Category", value); + } + + let mut default = false; + if let Some(windows) = bundle.windows.as_ref() { + if let Some(path) = windows.icon_path.as_ref() { + winres.set_icon(path.to_str().unwrap()); + 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.absolute_icon_path(icon)?.to_string_lossy().to_string(); + if !default { + winres.set_icon(&icon_path); + default = true; + } else { + winres.set_icon_with_id(&icon_path, &id.to_string()); + }; + } + } + } + + winres.compile()?; + + Ok(winres.linker) + } + async fn auto_provision_signing_name() -> Result { let identities = Command::new("security") .args(["find-identity", "-v", "-p", "codesigning"]) diff --git a/packages/cli/src/build/winres.rs b/packages/cli/src/build/winres.rs new file mode 100644 index 0000000000..b64bf418cc --- /dev/null +++ b/packages/cli/src/build/winres.rs @@ -0,0 +1,951 @@ +#![cfg_attr(rustfmt, rustfmt_skip)] +#![allow(clippy::all)] +//! 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` +//! +//! The [`WindowsResource::compile()`] method is intended to be used from a build script and +//! needs environment variables from cargo to be set. It not only compiles the resource +//! but directs cargo to link the resource compiler's output. +//! +//! # Example +//! +//! ```rust +//! # extern crate winres; +//! # use std::io; +//! # fn test_main() -> io::Result<()> { +//! if cfg!(target_os = "windows") { +//! let mut res = winres::WindowsResource::new(); +//! res.set_icon("test.ico") +//! # .set_output_directory(".") +//! .set("InternalName", "TEST.EXE") +//! // manually set version 1.0.0.0 +//! .set_version_info(winres::VersionInfo::PRODUCTVERSION, 0x0001000000000000); +//! res.compile()?; +//! } +//! # Ok(()) +//! # } +//! ``` +//! +//! # Defaults +//! +//! We try to guess some sensible default values from Cargo's build time environment variables +//! This is described in [`WindowsResource::new()`]. Furthermore we have to know where to find the +//! resource compiler for the MSVC Toolkit. This can be done by looking up a registry key but +//! for MinGW this has to be done manually. +//! +//! The following paths are the hardcoded defaults: +//! MSVC the last registry key at +//! `HKLM\SOFTWARE\Microsoft\Windows Kits\Installed Roots`, for MinGW we try our luck by simply +//! using the `%PATH%` environment variable. +//! +//! Note that the toolkit bitness as to match the one from the current Rust compiler. If you are +//! using Rust GNU 64-bit you have to use MinGW64. For MSVC this is simpler as (recent) Windows +//! SDK always installs both versions on a 64-bit system. +//! +//! [`WindowsResource::compile()`]: struct.WindowsResource.html#method.compile +//! [`WindowsResource::new()`]: struct.WindowsResource.html#method.new +#![allow(dead_code)] + +use krates::semver::Version; + +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; + +/// Values based on +/// use to_str() +#[allow(clippy::upper_case_acronyms)] +pub enum IDI { + APPLICATION = 32512, + ERROR = 32513, + QUESTION = 32514, + WARNING = 32515, + INFORMATION = 32516, + WINLOGO = 32517, + SHIELD = 32518, +} + +impl IDI { + pub fn as_str(&self) -> &'static str { + match self { + IDI::APPLICATION => "32512", + IDI::ERROR => "32513", + IDI::QUESTION => "32514", + IDI::WARNING => "32515", + IDI::INFORMATION => "32516", + IDI::WINLOGO => "32517", + IDI::SHIELD => "32518", + } + } +} + +/// 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, +} + +impl VersionInfo { + /// Creates u64 version from string + pub 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) + } + + pub fn version_from_krate(v: &Version) -> u64 { + (v.major << 48) | (v.minor << 32) | (v.patch << 16) + } +} + +/// Windows uses `32512` as the default icon ID. +const DEFAULT_ICON_ID: &str = "32512"; +#[derive(Debug)] +struct Icon { + path: String, + name_id: String, +} + +impl Icon { + fn from_pathbuf(path: &Path, id: &str) -> Self { + Icon { + path: path + .canonicalize() + .map(|p| p.to_string_lossy().to_string()) + .unwrap_or(path.to_string_lossy().to_string()), + name_id: id.into(), + } + } + + fn from_str(path: &str, id: &str) -> Self { + Icon { + path: PathBuf::from(path) + .canonicalize() + .map(|p| p.to_string_lossy().to_string()) + .unwrap_or(path.to_string()), + name_id: id.into(), + } + } +} +#[derive(Debug, Default)] +pub struct WindowsResourceLinker { + pub lib: String, + pub path: String, + pub files: Vec, + pub default_icon: Option, +} + +#[derive(Debug)] +pub struct WindowsResource { + pub linker: WindowsResourceLinker, + toolkit_path: PathBuf, + properties: HashMap, + version_info: HashMap, + rc_file: Option, + icons: Vec, + language: u16, + manifest: Option, + manifest_file: Option, + output_directory: String, + windres_path: String, + ar_path: String, + add_toolkit_include: bool, + append_rc_content: String, + assets_path: Option, + link: bool, +} + + +impl WindowsResource { + /// Create a new resource with version info struct + /// + /// + /// We initialize the resource file with values provided by cargo + /// + /// | Field | Cargo / Values | + /// |----------------------|------------------------------| + /// | `"FileVersion"` | `package.version` | + /// | `"ProductVersion"` | `package.version` | + /// | `"ProductName"` | `package.name` | + /// | `"FileDescription"` | `package.description` | + /// + /// Furthermore if a section `package.metadata.winres` exists + /// in `Cargo.toml` it will be parsed. Values in this section take precedence + /// over the values provided natively by cargo. Only the string table + /// of the version struct can be set this way. + /// Additionally, the language field is set to neutral (i.e. `0`) + /// and no icon is set. These settings have to be done programmatically. + /// + /// `Cargo.toml` files have to be written in UTF-8, so we support all valid UTF-8 strings + /// provided. + /// + /// ```,toml + /// #Cargo.toml + /// [package.metadata.winres] + /// OriginalFilename = "testing.exe" + /// FileDescription = "⛄❤☕" + /// LegalCopyright = "Copyright © 2016" + /// ``` + /// + /// The version info struct is set to some values + /// sensible for creating an executable file. + /// + /// | Property | Cargo / Values | + /// |----------------------|------------------------------| + /// | `FILEVERSION` | `package.version` | + /// | `PRODUCTVERSION` | `package.version` | + /// | `FILEOS` | `VOS_NT_WINDOWS32 (0x40004)` | + /// | `FILETYPE` | `VFT_APP (0x1)` | + /// | `FILESUBTYPE` | `VFT2_UNKNOWN (0x0)` | + /// | `FILEFLAGSMASK` | `VS_FFI_FILEFLAGSMASK (0x3F)`| + /// | `FILEFLAGS` | `0x0` | + /// + pub fn new() -> Self { + let props: HashMap = HashMap::new(); + let mut ver: HashMap = HashMap::new(); + + ver.insert(VersionInfo::FILEVERSION, 0); + ver.insert(VersionInfo::PRODUCTVERSION, 0); + ver.insert(VersionInfo::FILEOS, 0x00040004); + ver.insert(VersionInfo::FILETYPE, 1); + ver.insert(VersionInfo::FILESUBTYPE, 0); + ver.insert(VersionInfo::FILEFLAGSMASK, 0x3F); + ver.insert(VersionInfo::FILEFLAGS, 0); + + let sdk = if cfg!(target_env = "msvc") { + match get_sdk() { + Ok(mut v) => v.pop().unwrap(), + Err(_) => PathBuf::new(), + } + } else if cfg!(windows) { + PathBuf::from("\\") + } else { + PathBuf::from("/") + }; + + let prefix = if let Ok(cross) = env::var("CROSS_COMPILE") { + cross + } else if cfg!(not(target_env = "msvc")) + && env::var_os("HOST") + .zip(env::var_os("TARGET")) + .map_or(false, |(h, t)| h != t) + { + match env::var("TARGET").unwrap().as_str() { + "x86_64-pc-windows-gnu" => "x86_64-w64-mingw32-", + "i686-pc-windows-gnu" => "i686-w64-mingw32-", + "i586-pc-windows-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...) + "aarch64-pc-windows-gnu" => "llvm-", + // *-gnullvm targets by definition use LLVM-based toolchains + "x86_64-pc-windows-gnullvm" + | "i686-pc-windows-gnullvm" + | "aarch64-pc-windows-gnullvm" => "llvm-", + // fail safe + _ => { + println!( + "cargo:warning=unknown Windows target used for cross-compilation; \ + invoking unprefixed windres" + ); + "" + } + } + .into() + } else { + "".into() + }; + + 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) + }; + + WindowsResource { + toolkit_path: sdk, + properties: props, + version_info: ver, + rc_file: None, + icons: Vec::new(), + language: 0, + manifest: None, + manifest_file: None, + output_directory: ".".to_string(), + windres_path, + ar_path, + add_toolkit_include: false, + append_rc_content: String::new(), + assets_path: None, + link: true, + linker: WindowsResourceLinker::default(), + } + } + + + /// Set string properties of the version info struct. + /// + /// Possible field names are: + /// + /// - `"FileVersion"` + /// - `"FileDescription"` + /// - `"ProductVersion"` + /// - `"ProductName"` + /// - `"OriginalFilename"` + /// - `"LegalCopyright"` + /// - `"LegalTrademark"` + /// - `"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. + pub fn set(&mut self, name: &str, value: &str) -> &mut Self { + self.properties.insert(name.to_string(), value.to_string()); + self + } + + /// Set the correct path for the toolkit. + /// + /// For the GNU toolkit this has to be the path where MinGW + /// put `windres.exe` and `ar.exe`. This could be something like: + /// `C:\Program Files\mingw-w64\x86_64-5.3.0-win32-seh-rt_v4-rev0\mingw64\bin` + /// + /// For MSVC the Windows SDK has to be installed. It comes with the resource compiler + /// `rc.exe`. This should be set to the root directory of the Windows SDK, e.g., + /// `C:\Program Files (x86)\Windows Kits\10` + /// or, if multiple 10 versions are installed, + /// set it directly to the correct bin directory + /// `C:\Program Files (x86)\Windows Kits\10\bin\10.0.14393.0\x64` + /// + /// If it is left unset, it will look up a path in the registry, + /// i.e. `HKLM\SOFTWARE\Microsoft\Windows Kits\Installed Roots` + pub fn set_toolkit_path(&mut self, path: &str) -> &mut Self { + self.toolkit_path = PathBuf::from(path); + self + } + + /// Set the user interface language of the file + /// + /// # Example + /// + /// ``` + /// extern crate winapi; + /// extern crate winres; + /// # use std::io; + /// fn main() { + /// if cfg!(target_os = "windows") { + /// let mut res = winres::WindowsResource::new(); + /// # res.set_output_directory("."); + /// res.set_language(winapi::um::winnt::MAKELANGID( + /// winapi::um::winnt::LANG_ENGLISH, + /// winapi::um::winnt::SUBLANG_ENGLISH_US + /// )); + /// res.compile().unwrap(); + /// } + /// } + /// ``` + /// For possible values look at the `winapi::um::winnt` constants, specifically those + /// starting with `LANG_` and `SUBLANG_`. + /// + /// [`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` | + pub 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)```. [`IDI::APPLICATION`].as_str() + pub fn set_icon(&mut self, path: &str) -> &mut Self { + self.set_icon_with_id(path, IDI::APPLICATION.as_str()) + } + + /// 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: + /// + /// ```nocheck + /// res.set_icon("icon.ico") // This is application icon. + /// .set_icon_with_id("icon2.icon", "2") + /// .set_icon_with_id("icon3.icon", "3") + /// // ... + /// ``` + /// see [`IDI`]` for special icons ids + pub fn set_icon_with_id(&mut self, path: &str, name_id: &str) -> &mut Self { + self.icons.push(Icon { + path: path.into(), + name_id: name_id.into(), + }); + self + } + + /// Set a version info struct property + /// Currently we only support numeric values; you have to look them up. + pub fn set_version_info(&mut self, field: VersionInfo, value: u64) -> &mut Self { + self.version_info.insert(field, value); + self + } + + /// Set the embedded manifest file + /// + /// # Example + /// + /// The following manifest will brand the exe as requesting administrator privileges. + /// Thus, everytime it is executed, a Windows UAC dialog will appear. + /// + /// ```rust + /// let mut res = winres::WindowsResource::new(); + /// res.set_manifest(r#" + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// "#); + /// ``` + pub fn set_manifest(&mut self, manifest: &str) -> &mut Self { + self.manifest_file = None; + self.manifest = Some(manifest.to_string()); + self + } + + /// Some as [`set_manifest()`] but a filename can be provided and + /// file is included by the resource compiler itself. + /// This method works the same way as [`set_icon()`] + /// + /// [`set_manifest()`]: #method.set_manifest + /// [`set_icon()`]: #method.set_icon + pub fn set_manifest_file(&mut self, file: &str) -> &mut Self { + self.manifest_file = Some(file.to_string()); + self.manifest = None; + self + } + + /// Set the path to the windres executable. + pub fn set_windres_path(&mut self, path: &str) -> &mut Self { + self.windres_path = path.to_string(); + self + } + + /// Set the path to the ar executable. + pub fn set_ar_path(&mut self, path: &str) -> &mut Self { + self.ar_path = path.to_string(); + self + } + + /// Set the path to the ar executable. + pub fn add_toolkit_include(&mut self, add: bool) -> &mut Self { + self.add_toolkit_include = add; + self + } + + /// Set cargo manifest dir, if None defaults to env variable + pub fn set_assets_path(&mut self, path: Option) -> &mut Self { + self.assets_path = path; + self + } + + /// Write a resource file with the set values + pub fn write_resource_file>(&self, path: P) -> io::Result<()> { + 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(k), + escape_string(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(&icon.name_id), + escape_string(&icon.path) + )?; + } + if let Some(e) = self.version_info.get(&VersionInfo::FILETYPE) { + if let Some(manf) = self.manifest.as_ref() { + writeln!(f, "{} 24", e)?; + writeln!(f, "{{")?; + for line in manf.lines() { + writeln!(f, "\" {} \"", escape_string(line.trim()))?; + } + writeln!(f, "}}")?; + } else if let Some(manf) = self.manifest_file.as_ref() { + writeln!(f, "{} 24 \"{}\"", e, escape_string(manf))?; + } + } + writeln!(f, "{}", self.append_rc_content)?; + Ok(()) + } + + /// Set a path to an already existing resource file. + /// + /// We will neither modify this file nor parse its contents. This function + /// simply replaces the internally generated resource file that is passed to + /// the compiler. You can use this function to write a resource file yourself. + pub fn set_resource_file(&mut self, path: &str) -> &mut Self { + self.rc_file = Some(path.to_string()); + self + } + + /// Append an additional snippet to the generated rc file. + /// + /// # Example + /// + /// Define a menu resource: + /// + /// ```rust + /// # extern crate winres; + /// # if cfg!(target_os = "windows") { + /// let mut res = winres::WindowsResource::new(); + /// res.append_rc_content(r##"sample MENU + /// { + /// MENUITEM "&Soup", 100 + /// MENUITEM "S&alad", 101 + /// POPUP "&Entree" + /// { + /// MENUITEM "&Fish", 200 + /// MENUITEM "&Chicken", 201, CHECKED + /// POPUP "&Beef" + /// { + /// MENUITEM "&Steak", 301 + /// MENUITEM "&Prime Rib", 302 + /// } + /// } + /// MENUITEM "&Dessert", 103 + /// }"##); + /// # res.compile()?; + /// # } + /// # Ok::<_, std::io::Error>(()) + /// ``` + pub 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 + } + + /// Override the output directory. + /// + /// As a default, we use `%OUT_DIR%` set by cargo, but it may be necessary to override the + /// the setting. + pub fn set_output_directory(&mut self, path: &str) -> &mut Self { + self.output_directory = path.to_string(); + self + } + + /// Print the links or not + pub fn set_link(&mut self, value: bool) -> &mut Self { + self.link = value; + 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. + /// + /// Further more we will print the correct statements for + /// `cargo:rustc-link-lib=` and `cargo:rustc-link-search` on the console, + /// so that the cargo build script can link the compiled resource file. + pub fn compile(&mut self) -> io::Result<()> { + let output = PathBuf::from(&self.output_directory); + let rc = output.join("resource.rc"); + if self.rc_file.is_none() { + self.write_resource_file(&rc)?; + } + let rc = if let Some(s) = self.rc_file.as_ref() { + s.clone() + } else { + rc.to_str().unwrap().to_string() + }; + + if cfg!(target_env = "msvc") { + tracing::debug!("Compiling Windows resource file with msvc toolkit"); + self.compile_with_toolkit_msvc(rc.as_str()) + } else if cfg!(target_env = "gnu") { + tracing::debug!("Compiling Windows resource file with gnu toolkit"); + self.compile_with_toolkit_gnu(rc.as_str()) + } else { + Err(io::Error::new( + io::ErrorKind::Other, + "Can only compile resource file when target_env is \"gnu\" or \"msvc\"", + )) + } + } + + fn compile_with_toolkit_gnu(&mut self, input: &str) -> io::Result<()> { + let input = PathBuf::from(input); + tracing::debug!("Input file: '{}'", input.display()); + let output = PathBuf::from(&self.output_directory).join("resource.o"); + tracing::debug!("Output object file: '{}'", output.display()); + let manifest = match &self.assets_path { + Some(val) => val, + None => &std::env::var("CARGO_MANIFEST_DIR").unwrap(), + }; + + tracing::debug!("Selected toolkit path: '{}'", &self.toolkit_path.display()); + tracing::debug!("Selected windres path: '{:?}'", &self.windres_path); + tracing::debug!("Selected ar path: '{:?}'", &self.ar_path); + + let status = process::Command::new(&self.windres_path) + .current_dir(&self.toolkit_path) + .arg(format!("-I{}", manifest)) + .arg(format!("{}", input.display())) + .arg(format!("{}", output.display())) + .output()?; + + if !status.status.success() { + return Err(io::Error::new( + io::ErrorKind::Other, + format!("Compiling resource file {:?}", &status.stderr), + )); + } + + let libname = PathBuf::from(&self.output_directory).join("libresource.a"); + tracing::debug!("Output lib file: '{}'", output.display()); + let status = process::Command::new(&self.ar_path) + .current_dir(&self.toolkit_path) + .arg("rsc") + .arg(format!("{}", libname.display())) + .arg(format!("{}", output.display())) + .output()?; + + if !status.status.success() { + return Err(io::Error::new( + io::ErrorKind::Other, + format!("Creating static library for resource file {:?}", &status.stderr), + )); + } + + self.linker.lib = "static=resource".to_string(); + self.linker.path = self.output_directory.clone(); + self.linker.files = vec![ + output.to_string_lossy().to_string(), + libname.to_string_lossy().to_string(), + ]; + + if self.link { + println!("cargo:rustc-link-search=native={}", self.output_directory); + println!("cargo:rustc-link-lib=static=resource"); + } + + Ok(()) + } + + fn compile_with_toolkit_msvc(&mut self, input: &str) -> io::Result<()> { + let rc_exe = PathBuf::from(&self.toolkit_path).join("rc.exe"); + let rc_exe = if !rc_exe.exists() { + if cfg!(target_arch = "x86_64") { + PathBuf::from(&self.toolkit_path).join(r"bin\x64\rc.exe") + } else { + PathBuf::from(&self.toolkit_path).join(r"bin\x86\rc.exe") + } + } else { + rc_exe + }; + let manifest = match &self.assets_path { + Some(val) => val, + None => &std::env::var("CARGO_MANIFEST_DIR").unwrap(), + }; + + tracing::debug!("Selected toolkit path: '{}'", rc_exe.display()); + let input = PathBuf::from(input); + tracing::debug!("Input file: '{}'", input.display()); + let output = PathBuf::from(&self.output_directory).join("resource.lib"); + tracing::debug!("Output file: '{}'", output.display()); + let mut command = process::Command::new(&rc_exe); + let command = command.arg(format!("/I{}", manifest)); + + if self.add_toolkit_include { + let root = win_sdk_include_root(&rc_exe); + 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!("{}", input.display())) + .output()?; + + + if !status.status.success() { + return Err(io::Error::new( + io::ErrorKind::Other, + format!("Compiling resource file {:?}", &status.stderr), + )); + } + + self.linker.lib = "dylib=resource".to_string(); + self.linker.path = self.output_directory.clone(); + self.linker.files = vec![output.to_string_lossy().to_string()]; + + if self.link { + println!("cargo:rustc-link-search=native={}", self.output_directory); + println!("cargo:rustc-link-lib=dylib=resource"); + } + + Ok(()) + } +} + +/// Find a Windows SDK +fn get_sdk() -> 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::new( + io::ErrorKind::Other, + format!( + "Querying the registry failed with error message:\n{}", + String::from_utf8(output.stderr) + .map_err(|e| io::Error::new(io::ErrorKind::Other, e.to_string()))? + ), + )); + } + + let lines = String::from_utf8(output.stdout) + .map_err(|e| io::Error::new(io::ErrorKind::Other, e.to_string()))?; + let mut kits: Vec = Vec::new(); + 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 cfg!(target_arch = "x86_64") { + p.join(r"bin\x64\rc.exe") + } else { + p.join(r"bin\x86\rc.exe") + }; + + if rc.exists() { + kits.push(rc.parent().unwrap().to_owned()); + } + + if let Ok(bin) = p.join("bin").read_dir() { + for e in bin.filter_map(|e| e.ok()) { + let p = if cfg!(target_arch = "x86_64") { + e.path().join(r"x64\rc.exe") + } else { + e.path().join(r"x86\rc.exe") + }; + if p.exists() { + kits.push(p.parent().unwrap().to_owned()); + } + } + } + } + } + if kits.is_empty() { + return Err(io::Error::new( + io::ErrorKind::Other, + "Can not find Windows SDK", + )); + } + + Ok(kits) +} + +pub(crate) fn escape_string(string: &str) -> String { + let mut escaped = String::new(); + for chr in string.chars() { + // In quoted RC strings, double-quotes are escaped by using two + // consecutive double-quotes. Other characters are escaped in the + // usual C way using backslashes. + 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 ba6f0e9e3b..7287813328 100644 --- a/packages/cli/src/cli/bundle.rs +++ b/packages/cli/src/cli/bundle.rs @@ -148,6 +148,24 @@ impl Bundle { }) } + #[allow(deprecated)] + fn windows_icon_override( + krate: &BuildRequest, + bundle_settings: &mut BundleSettings, + windows_icon: Option, + ) { + let has_windows_icon_override = match krate.config.bundle.windows.as_ref() { + Some(windows) => windows.icon_path.is_some(), + None => false, + }; + + if !has_windows_icon_override { + let icon = windows_icon.expect("Missing .ico app icon"); + // for now it still needs to be set even though it's deprecated + bundle_settings.windows.icon_path = PathBuf::from(icon); + } + } + fn bundle_desktop( build: &BuildRequest, package_types: &Option>, @@ -184,17 +202,12 @@ impl Bundle { } if cfg!(windows) { - let windows_icon_override = krate.config.bundle.windows.as_ref().map(|w| &w.icon_path); - if windows_icon_override.is_none() { - let icon_path = bundle_settings - .icon - .as_ref() - .and_then(|icons| icons.first()); - - if let Some(icon_path) = icon_path { - bundle_settings.icon = Some(vec![icon_path.into()]); - }; - } + let windows_icon = match bundle_settings.icon.as_ref() { + Some(icons) => icons.iter().find(|i| i.ends_with(".ico")).cloned(), + None => None, + }; + + Self::windows_icon_override(krate, &mut bundle_settings, windows_icon); } if bundle_settings.resources_map.is_none() { diff --git a/packages/cli/src/config/app.rs b/packages/cli/src/config/app.rs index 80ec2041c1..2cafe228c9 100644 --- a/packages/cli/src/config/app.rs +++ b/packages/cli/src/config/app.rs @@ -4,9 +4,15 @@ use std::path::PathBuf; #[derive(Debug, Clone, Serialize, Deserialize)] pub(crate) struct ApplicationConfig { /// The path where global assets will be added when components are added with `dx components add` + #[serde(default)] + pub(crate) name: Option, + #[serde(default)] pub(crate) asset_dir: Option, + #[serde(default)] + pub(crate) sub_package: Option, + #[serde(default)] pub(crate) out_dir: Option, diff --git a/packages/cli/src/config/bundle.rs b/packages/cli/src/config/bundle.rs index 2a17b3af9e..919fd2ea86 100644 --- a/packages/cli/src/config/bundle.rs +++ b/packages/cli/src/config/bundle.rs @@ -30,6 +30,10 @@ pub(crate) struct BundleConfig { pub(crate) windows: Option, #[serde(default)] pub(crate) android: Option, + pub(crate) version: Option, + pub(crate) file_version: Option, + pub(crate) original_file_name: Option, + pub(crate) trademark: Option, } #[derive(Debug, Clone, Serialize, Deserialize, Default)] diff --git a/packages/cli/src/config/dioxus_config.rs b/packages/cli/src/config/dioxus_config.rs index d96afbb54b..5dfb763203 100644 --- a/packages/cli/src/config/dioxus_config.rs +++ b/packages/cli/src/config/dioxus_config.rs @@ -21,7 +21,9 @@ impl Default for DioxusConfig { fn default() -> Self { Self { application: ApplicationConfig { + name: None, asset_dir: None, + sub_package: None, out_dir: None, public_dir: Some("public".into()), tailwind_input: None, diff --git a/packages/desktop/Cargo.toml b/packages/desktop/Cargo.toml index 715c379f27..e486f88af8 100644 --- a/packages/desktop/Cargo.toml +++ b/packages/desktop/Cargo.toml @@ -15,26 +15,35 @@ dioxus-core = { workspace = true, features = ["serialize"] } dioxus-html = { workspace = true, features = ["serialize"] } dioxus-document = { workspace = true } dioxus-signals = { workspace = true, optional = true } -dioxus-interpreter-js = { workspace = true, features = ["binary-protocol", "serialize"] } +dioxus-interpreter-js = { workspace = true, features = [ + "binary-protocol", + "serialize", +] } dioxus-cli-config = { workspace = true } dioxus-asset-resolver = { workspace = true, features = ["native"] } generational-box = { workspace = true } dioxus-devtools = { workspace = true, optional = true } +anyhow = { workspace = true } +image = { workspace = true } serde = "1.0.219" serde_json = "1.0.140" thiserror = { workspace = true } tracing = { workspace = true } -wry = { workspace = true, default-features = false, features = ["os-webview", "protocol", "drag-drop"] } +wry = { workspace = true, default-features = false, features = [ + "os-webview", + "protocol", + "drag-drop", +] } futures-channel = { workspace = true } tokio = { workspace = true, features = [ - "sync", - "rt-multi-thread", - "rt", - "time", - "macros", - "fs", - "io-util", + "sync", + "rt-multi-thread", + "rt", + "time", + "macros", + "fs", + "io-util", ], optional = true } infer = { workspace = true } dunce = { workspace = true } @@ -57,11 +66,19 @@ webbrowser = { workspace = true } signal-hook = "0.3.18" [target.'cfg(target_os = "linux")'.dependencies] -wry = { workspace = true, features = ["os-webview", "protocol", "drag-drop", "linux-body"] } +wry = { workspace = true, features = [ + "os-webview", + "protocol", + "drag-drop", + "linux-body", +] } [target.'cfg(any(target_os = "windows",target_os = "macos",target_os = "linux",target_os = "dragonfly", target_os = "freebsd", target_os = "netbsd", target_os = "openbsd"))'.dependencies] global-hotkey = "0.7.0" -rfd = { version = "0.15.3", default-features = false, features = ["xdg-portal", "tokio"] } +rfd = { version = "0.15.3", default-features = false, features = [ + "xdg-portal", + "tokio", +] } muda = { workspace = true } [target.'cfg(any(target_os = "windows",target_os = "macos",target_os = "linux"))'.dependencies] @@ -104,12 +121,12 @@ features = ["tokio_runtime", "devtools"] cargo-args = ["-Zunstable-options", "-Zrustdoc-scrape-examples"] default-features = false targets = [ - "x86_64-unknown-linux-gnu", - "x86_64-pc-windows-msvc", - "aarch64-apple-darwin", - "aarch64-apple-ios", - "aarch64-linux-android", - "armv7-linux-androideabi", + "x86_64-unknown-linux-gnu", + "x86_64-pc-windows-msvc", + "aarch64-apple-darwin", + "aarch64-apple-ios", + "aarch64-linux-android", + "armv7-linux-androideabi", ] [dev-dependencies] diff --git a/packages/desktop/build.rs b/packages/desktop/build.rs index b1705cb50b..6ecb818eb0 100644 --- a/packages/desktop/build.rs +++ b/packages/desktop/build.rs @@ -44,9 +44,16 @@ fn compile_ts() { .run(); } +fn check_default_icon() { + if option_env!("DIOXUS_APP_ICON").is_none() { + println!("cargo:rustc-env=DIOXUS_APP_ICON=./assets/default_icon.png") + } +} + fn main() { check_gnu(); compile_ts(); + check_default_icon(); } const EXAMPLES_TOML: &str = r#" diff --git a/packages/desktop/src/default_icon.rs b/packages/desktop/src/default_icon.rs new file mode 100644 index 0000000000..7d96714a7f --- /dev/null +++ b/packages/desktop/src/default_icon.rs @@ -0,0 +1,172 @@ +use anyhow::Result; +use image::load_from_memory; +use image::GenericImageView; +use image::ImageReader; +use std::path::Path; + +/// Trait that creates icons for various types +pub trait DioxusIconTrait { + fn get_icon() -> Self + where + Self: Sized; + fn from_memory(value: &[u8]) -> Self + where + Self: Sized; + fn path>(path: P, size: Option<(u32, u32)>) -> Result + where + Self: Sized; +} + +// preferably this would have platform specific implementations, not just for windows +#[cfg(any(debug_assertions, not(target_os = "windows")))] +static DEFAULT_ICON: &[u8] = include_bytes!(env!("DIOXUS_APP_ICON")); + +#[cfg(any(target_os = "linux", target_os = "macos", target_os = "windows"))] +use crate::trayicon::DioxusTrayIcon; + +fn load_image_from_memory(value: &[u8]) -> (Vec, u32, u32) { + let img = load_from_memory(value).expect("MISSING DEFAULT ICON"); + let rgba = img.to_rgba8(); + let (width, height) = img.dimensions(); + (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() -> Self + where + Self: Sized, + { + #[cfg(any(debug_assertions, target_os = "linux", target_os = "macos"))] + { + let (img, width, height) = load_image_from_memory(DEFAULT_ICON); + DioxusTrayIcon::from_rgba(img, width, height).expect("image parse failed") + } + #[cfg(all(not(debug_assertions), target_os = "windows"))] + DioxusTrayIcon::from_resource(32512, None).expect("image parse failed") + } + + fn from_memory(value: &[u8]) -> Self + where + Self: Sized, + { + let (icon, width, height) = load_image_from_memory(value); + DioxusTrayIcon::from_rgba(icon, width, height).expect("image parse failed") + } + + 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() -> Self + where + Self: Sized, + { + #[cfg(any(debug_assertions, not(target_os = "windows")))] + { + let (img, width, height) = load_image_from_memory(DEFAULT_ICON); + DioxusMenuIcon::from_rgba(img, width, height).expect("image parse failed") + } + #[cfg(all(not(debug_assertions), target_os = "windows"))] + DioxusMenuIcon::from_resource(32512, None).expect("image parse failed") + } + + fn from_memory(value: &[u8]) -> Self + where + Self: Sized, + { + let (icon, width, height) = load_image_from_memory(value); + DioxusMenuIcon::from_rgba(icon, width, height).expect("image parse failed") + } + + 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(all(not(debug_assertions), target_os = "windows"))] +use tao::platform::windows::IconExtWindows; + +impl DioxusIconTrait for Icon { + fn get_icon() -> Self + where + Self: Sized, + { + #[cfg(any(debug_assertions, not(target_os = "windows")))] + { + let (img, width, height) = load_image_from_memory(DEFAULT_ICON); + Icon::from_rgba(img, width, height).expect("image parse failed") + } + #[cfg(all(not(debug_assertions), target_os = "windows"))] + Icon::from_resource(32512, None).expect("image parse failed") + } + + fn from_memory(value: &[u8]) -> Self + where + Self: Sized, + { + let (icon, width, height) = load_image_from_memory(value); + Icon::from_rgba(icon, width, height).expect("image parse failed") + } + + 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 +pub fn default_icon() -> T { + T::get_icon() +} + +/// Helper function to load image from include_bytes!("image.png") +pub fn icon_from_memory(value: &[u8]) -> T { + 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 d4051bf4be..a44ced8bb2 100644 --- a/packages/desktop/src/lib.rs +++ b/packages/desktop/src/lib.rs @@ -8,6 +8,7 @@ mod android_sync_lock; mod app; mod assets; mod config; +mod default_icon; mod desktop_context; mod document; mod edits; @@ -25,6 +26,8 @@ mod shortcut; mod waker; mod webview; +pub use default_icon::{default_icon, icon_from_memory, icon_from_path}; + // mobile shortcut is only supported on mobile platforms #[cfg(any(target_os = "ios", target_os = "android"))] mod mobile_shortcut; diff --git a/packages/desktop/src/menubar.rs b/packages/desktop/src/menubar.rs index b63d07d174..0817bd6f48 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 8cc65f7d23..ff40a3010f 100644 --- a/packages/desktop/src/trayicon.rs +++ b/packages/desktop/src/trayicon.rs @@ -33,12 +33,7 @@ pub fn init_tray_icon(menu: DioxusTrayMenu, icon: Option) -> Dio .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"), + None => crate::default_icon(), }); provide_context(builder.build().expect("tray icon builder failed")) diff --git a/packages/desktop/src/webview.rs b/packages/desktop/src/webview.rs index df662aaa9b..b953a89aef 100644 --- a/packages/desktop/src/webview.rs +++ b/packages/desktop/src/webview.rs @@ -218,14 +218,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(Some(crate::default_icon())); } let window = window.build(&shared.target).unwrap();