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 <jkelleyrtp@gmail.com>
This commit is contained in:
KlemenandJonathan Kelley authored and GitHub committed 2026-04-21 18:20:13 -07:00
1 parent 6d1f19067a
commit 3f82c45520
17 files changed
+1125 -25

No files matched your search

+2
View File
@@ -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 }
+187
View File
@@ -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<Self>
where
Self: Sized;
fn from_memory(value: &[u8]) -> Result<Self>
where
Self: Sized;
fn path<P: AsRef<Path>>(path: P, size: Option<(u32, u32)>) -> Result<Self>
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<u8>, 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<P: AsRef<Path>>(path: P) -> Result<(Vec<u8>, 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<Self>
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<Self>
where
Self: Sized,
{
let (icon, width, height) = load_image_from_memory(value)?;
DioxusTrayIcon::from_rgba(icon, width, height).map_err(Into::into)
}
fn path<P: AsRef<Path>>(path: P, size: Option<(u32, u32)>) -> Result<Self>
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<Self>
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<Self>
where
Self: Sized,
{
let (icon, width, height) = load_image_from_memory(value)?;
DioxusMenuIcon::from_rgba(icon, width, height).map_err(Into::into)
}
fn path<P: AsRef<Path>>(path: P, size: Option<(u32, u32)>) -> Result<Self>
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<Self>
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<Self>
where
Self: Sized,
{
let (icon, width, height) = load_image_from_memory(value)?;
Icon::from_rgba(icon, width, height).map_err(Into::into)
}
fn path<P: AsRef<Path>>(path: P, size: Option<(u32, u32)>) -> Result<Self>
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<T: DioxusIconTrait>() -> Result<T> {
T::get_icon()
}
/// Helper function to load image from include_bytes!("image.png")
pub fn icon_from_memory<T: DioxusIconTrait>(value: &[u8]) -> Result<T> {
T::from_memory(value)
}
/// Helper function to load image from path
pub fn icon_from_path<T: DioxusIconTrait, P: AsRef<Path>>(
path: P,
size: Option<(u32, u32)>,
) -> Result<T> {
T::path(path, size)
}
+2
View File
@@ -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,
};
+5
View File
@@ -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) {
+14 -13
View File
@@ -28,20 +28,21 @@ pub type DioxusTray = ();
pub fn init_tray_icon(menu: DioxusTrayMenu, icon: Option<DioxusTrayIcon>) -> 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"))
}
}
+1 -8
View File
@@ -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());