From 36d81c151de55e93de4501959b27361d6c03b67e Mon Sep 17 00:00:00 2001 From: TeamDman Date: Sun, 28 Sep 2025 12:29:27 -0400 Subject: [PATCH 01/11] Implement custom_window_icon feature --- Cargo.toml | 5 +++ crates/bevy_internal/Cargo.toml | 3 ++ crates/bevy_window/Cargo.toml | 3 ++ crates/bevy_window/src/window.rs | 24 +++++++++++++ crates/bevy_winit/Cargo.toml | 10 ++++-- crates/bevy_winit/src/lib.rs | 4 +++ crates/bevy_winit/src/system.rs | 54 ++++++++++++++++++++++++++++++ examples/window/window_settings.rs | 20 ++++++++++- 8 files changed, 120 insertions(+), 3 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index dbf0401117928..79f1f38d6f568 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -163,6 +163,7 @@ default = [ "bevy_window", "bevy_winit", "custom_cursor", + "custom_window_icon", "default_font", "hdr", "ktx2", @@ -563,6 +564,9 @@ reflect_auto_register_static = ["bevy_internal/reflect_auto_register_static"] # Enable winit custom cursor support custom_cursor = ["bevy_internal/custom_cursor"] +# Enable winit custom cursor support +custom_window_icon = ["bevy_internal/custom_window_icon"] + # Experimental support for nodes that are ignored for UI layouting ghost_nodes = ["bevy_internal/ghost_nodes"] @@ -3772,6 +3776,7 @@ wasm = false name = "window_settings" path = "examples/window/window_settings.rs" doc-scrape-examples = true +required-features = ["bevy_window", "bevy_winit"] [package.metadata.example.window_settings] name = "Window Settings" diff --git a/crates/bevy_internal/Cargo.toml b/crates/bevy_internal/Cargo.toml index 3a43a38147c50..abed344e8b424 100644 --- a/crates/bevy_internal/Cargo.toml +++ b/crates/bevy_internal/Cargo.toml @@ -361,6 +361,9 @@ reflect_documentation = ["bevy_reflect/documentation"] # Enable custom cursor support custom_cursor = ["bevy_window/custom_cursor", "bevy_winit/custom_cursor"] +# Enable custom icon support +custom_window_icon = ["bevy_window/custom_window_icon", "bevy_winit/custom_window_icon"] + # Experimental support for nodes that are ignored for UI layouting ghost_nodes = ["bevy_ui/ghost_nodes"] diff --git a/crates/bevy_window/Cargo.toml b/crates/bevy_window/Cargo.toml index 601803b6825bf..fd52f393593f0 100644 --- a/crates/bevy_window/Cargo.toml +++ b/crates/bevy_window/Cargo.toml @@ -27,6 +27,9 @@ serialize = ["serde", "bevy_ecs/serialize", "bevy_input/serialize"] # Enable custom cursor support custom_cursor = ["bevy_image", "bevy_asset"] +# Enable custom icon support +custom_window_icon = ["bevy_image", "bevy_asset"] + # Platform Compatibility ## Allows access to the `std` crate. Enabling this feature will prevent compilation diff --git a/crates/bevy_window/src/window.rs b/crates/bevy_window/src/window.rs index b3a306cb8c3b2..b9bd045a970f6 100644 --- a/crates/bevy_window/src/window.rs +++ b/crates/bevy_window/src/window.rs @@ -1,6 +1,10 @@ #[cfg(feature = "std")] use alloc::format; use alloc::{borrow::ToOwned, string::String}; +#[cfg(feature = "custom_window_icon")] +use bevy_asset::Handle; +#[cfg(feature = "custom_window_icon")] +use bevy_image::Image; use core::num::NonZero; use bevy_ecs::{ @@ -776,6 +780,26 @@ impl Default for CursorOptions { } } + +/// Icon data for a [`Window`]. +#[derive(Component, Debug, Clone, Default)] +#[cfg_attr( + feature = "bevy_reflect", + derive(Reflect), + reflect(Component, Debug, Default, Clone) +)] +#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))] +#[cfg_attr( + all(feature = "serialize", feature = "bevy_reflect"), + reflect(Serialize, Deserialize) +)] +#[cfg(feature = "custom_window_icon")] +pub struct WindowIcon { + /// Handle to the asset to be read into the window icon. + pub handle: Handle, +} + + /// Defines where a [`Window`] should be placed on the screen. #[derive(Default, Debug, Clone, Copy, PartialEq)] #[cfg_attr( diff --git a/crates/bevy_winit/Cargo.toml b/crates/bevy_winit/Cargo.toml index f3a7498b7c288..0741174a54a92 100644 --- a/crates/bevy_winit/Cargo.toml +++ b/crates/bevy_winit/Cargo.toml @@ -34,6 +34,12 @@ custom_cursor = [ "bytemuck", ] +custom_window_icon = [ + "bevy_window/custom_window_icon", + "bevy_image", + "bevy_asset", +] + [dependencies] # bevy bevy_a11y = { path = "../bevy_a11y", version = "0.18.0-dev" } @@ -52,9 +58,9 @@ bevy_platform = { path = "../bevy_platform", version = "0.18.0-dev", default-fea ] } # bevy optional -## used by custom_cursor +## used by custom_cursor and custom_window_icon bevy_asset = { path = "../bevy_asset", version = "0.18.0-dev", optional = true } -## used by custom_cursor +## used by custom_cursor and custom_window_icon bevy_image = { path = "../bevy_image", version = "0.18.0-dev", optional = true } ## used by custom_cursor wgpu-types = { version = "26", optional = true } diff --git a/crates/bevy_winit/src/lib.rs b/crates/bevy_winit/src/lib.rs index 8a602313e33a2..dcb89ec6fb576 100644 --- a/crates/bevy_winit/src/lib.rs +++ b/crates/bevy_winit/src/lib.rs @@ -43,6 +43,9 @@ use crate::{ winit_monitors::WinitMonitors, }; +#[cfg(feature = "custom_window_icon")] +use system::changed_window_icon; + pub mod accessibility; mod converters; mod cursor; @@ -142,6 +145,7 @@ impl Plugin for WinitPlugin { // so we don't need to care about its ordering relative to `changed_windows` changed_windows.ambiguous_with(exit_on_all_closed), changed_cursor_options, + #[cfg(feature = "custom_window_icon")] changed_window_icon, despawn_windows, check_keyboard_focus_lost, ) diff --git a/crates/bevy_winit/src/system.rs b/crates/bevy_winit/src/system.rs index 798ce00945000..3bd0714022035 100644 --- a/crates/bevy_winit/src/system.rs +++ b/crates/bevy_winit/src/system.rs @@ -1,5 +1,7 @@ use std::collections::HashMap; +#[cfg(feature = "custom_window_icon")] +use bevy_asset::Assets; use bevy_derive::{Deref, DerefMut}; use bevy_ecs::{ change_detection::DetectChangesMut, @@ -10,7 +12,11 @@ use bevy_ecs::{ query::QueryFilter, system::{Local, NonSendMarker, Query, SystemParamItem}, }; +#[cfg(feature = "custom_window_icon")] +use bevy_image::Image; use bevy_input::keyboard::{Key, KeyCode, KeyboardFocusLost, KeyboardInput}; +#[cfg(feature = "custom_window_icon")] +use bevy_window::WindowIcon; use bevy_window::{ ClosingWindow, CursorOptions, Monitor, PrimaryMonitor, RawHandleWrapper, VideoMode, Window, WindowClosed, WindowClosing, WindowCreated, WindowEvent, WindowFocused, WindowMode, @@ -618,6 +624,54 @@ pub(crate) fn changed_cursor_options( }); } +#[cfg(feature = "custom_window_icon")] +pub(crate) fn changed_window_icon( + changed_windows: Query<(Entity, &Window, &WindowIcon), Changed>, + assets: Res>, + _non_send_marker: NonSendMarker, +) { + WINIT_WINDOWS.with_borrow(|winit_windows| { + for (entity, window, window_icon) in changed_windows { + // Identify window + let Some(winit_window) = winit_windows.get_window(entity) else { + continue; + }; + + // Fetch the image asset + let Some(image) = assets.get(&window_icon.handle) else { + warn!(?window_icon.handle, "Could not set window icon for window {}: image asset not found", window.title); + continue; + }; + + // Acquire pixel data from the image + let Some(image_data) = image.data.clone() else { + warn!( + ?window_icon.handle, + "Image handle has no data, the window will not have our custom icon", + ); + continue; + }; + + // Convert between formats + let icon = match winit::window::Icon::from_rgba( + image_data, + image.texture_descriptor.size.width, + image.texture_descriptor.size.height, + ) { + Ok(icon) => icon, + Err(e) => { + error!("Failed to construct window icon: {:?}", e); + continue; + } + }; + + // Set the window icon + tracing::debug!(image_size = ?image.size(), "Setting window icon"); + winit_window.set_window_icon(Some(icon)); + } + }); +} + /// This keeps track of which keys are pressed on each window. /// When a window is unfocused, this is used to send key release events for all the currently held keys. #[derive(Default, Component)] diff --git a/examples/window/window_settings.rs b/examples/window/window_settings.rs index 6d64ca4754671..809aad2413619 100644 --- a/examples/window/window_settings.rs +++ b/examples/window/window_settings.rs @@ -41,7 +41,7 @@ fn main() { LogDiagnosticsPlugin::default(), FrameTimeDiagnosticsPlugin::default(), )) - .add_systems(Startup, init_cursor_icons) + .add_systems(Startup, (init_cursor_icons, init_window_icon)) .add_systems( Update, ( @@ -77,6 +77,7 @@ fn toggle_vsync(input: Res>, mut window: Single<&mut Window } else { PresentMode::AutoVsync }; + #[cfg(feature="bevy_log")] info!("PRESENT_MODE: {:?}", window.present_mode); } } @@ -95,6 +96,7 @@ fn switch_level(input: Res>, mut window: Single<&mut Window WindowLevel::Normal => WindowLevel::AlwaysOnTop, WindowLevel::AlwaysOnTop => WindowLevel::AlwaysOnBottom, }; + #[cfg(feature="bevy_log")] info!("WINDOW_LEVEL: {:?}", window.window_level); } } @@ -174,6 +176,22 @@ fn init_cursor_icons( ])); } +fn init_window_icon( + mut commands: Commands, + window: Single>, + #[cfg(feature = "custom_window_icon")] asset_server: Res, +) { + #[cfg(feature = "custom_window_icon")] + { + use bevy::window::WindowIcon; + + let icon_handle = asset_server.load("branding/icon.png"); + commands.entity(*window).insert(WindowIcon { + handle: icon_handle, + }); + } +} + /// This system cycles the cursor's icon through a small set of icons when clicking fn cycle_cursor_icon( mut commands: Commands, From 1dcd3acf92507f7ca8a2d0342d25af27945082e1 Mon Sep 17 00:00:00 2001 From: TeamDman Date: Sun, 28 Sep 2025 15:46:25 -0400 Subject: [PATCH 02/11] Automatically convert to RGBA for window icon, improved logging fidelity --- crates/bevy_winit/src/lib.rs | 3 +- crates/bevy_winit/src/system.rs | 66 ++++++++++++++++++++++++--------- 2 files changed, 50 insertions(+), 19 deletions(-) diff --git a/crates/bevy_winit/src/lib.rs b/crates/bevy_winit/src/lib.rs index dcb89ec6fb576..94a9aa03ca63a 100644 --- a/crates/bevy_winit/src/lib.rs +++ b/crates/bevy_winit/src/lib.rs @@ -145,7 +145,8 @@ impl Plugin for WinitPlugin { // so we don't need to care about its ordering relative to `changed_windows` changed_windows.ambiguous_with(exit_on_all_closed), changed_cursor_options, - #[cfg(feature = "custom_window_icon")] changed_window_icon, + #[cfg(feature = "custom_window_icon")] + changed_window_icon, despawn_windows, check_keyboard_focus_lost, ) diff --git a/crates/bevy_winit/src/system.rs b/crates/bevy_winit/src/system.rs index 3bd0714022035..67155319e7689 100644 --- a/crates/bevy_winit/src/system.rs +++ b/crates/bevy_winit/src/system.rs @@ -631,42 +631,72 @@ pub(crate) fn changed_window_icon( _non_send_marker: NonSendMarker, ) { WINIT_WINDOWS.with_borrow(|winit_windows| { - for (entity, window, window_icon) in changed_windows { + for (window_entity, window, window_icon) in changed_windows { // Identify window - let Some(winit_window) = winit_windows.get_window(entity) else { + let Some(winit_window) = winit_windows.get_window(window_entity) else { continue; }; - + // Fetch the image asset let Some(image) = assets.get(&window_icon.handle) else { - warn!(?window_icon.handle, "Could not set window icon for window {}: image asset not found", window.title); - continue; - }; - - // Acquire pixel data from the image - let Some(image_data) = image.data.clone() else { warn!( - ?window_icon.handle, - "Image handle has no data, the window will not have our custom icon", + ?window_entity, + ?window, + ?window_icon, + "Could not set window icon for window: image asset not found" ); continue; }; - // Convert between formats + // Convert to rgba image + let rgba_image = match image.clone().try_into_dynamic() { + Ok(dynamic_image) => { + // winit icon expects 32bpp RGBA data + dynamic_image.into_rgba8() + } + Err(error) => { + error!( + ?window_entity, + ?window, + ?window_icon, + ?image, + ?error, + "Could not set window icon for window: failed to convert image to RGBA", + ); + continue; + } + }; + + // Convert to winit image + let width = rgba_image.width(); + let height = rgba_image.height(); let icon = match winit::window::Icon::from_rgba( - image_data, - image.texture_descriptor.size.width, - image.texture_descriptor.size.height, + rgba_image.into_raw(), + width, + height, ) { Ok(icon) => icon, - Err(e) => { - error!("Failed to construct window icon: {:?}", e); + Err(error) => { + error!( + ?window_entity, + ?window, + ?window_icon, + ?image, + ?error, + "Could not set window icon for window: failed to construct winit window icon from RGBA buffer", + ); continue; } }; // Set the window icon - tracing::debug!(image_size = ?image.size(), "Setting window icon"); + tracing::debug!( + ?window_entity, + ?window.title, + ?window_icon.handle, + image_size = ?image.size(), + "Setting window icon" + ); winit_window.set_window_icon(Some(icon)); } }); From 8cf8576cdb76dd53603b82a7d651623b39944774 Mon Sep 17 00:00:00 2001 From: TeamDman Date: Sun, 28 Sep 2025 15:51:08 -0400 Subject: [PATCH 03/11] Ensure systems in window_settings.rs example are only installed when necessary. Add log_asset_messages to help understand the timing involved with window icon. --- examples/window/window_settings.rs | 38 +++++++++++++++++++++--------- 1 file changed, 27 insertions(+), 11 deletions(-) diff --git a/examples/window/window_settings.rs b/examples/window/window_settings.rs index 809aad2413619..33d2c30bc53ff 100644 --- a/examples/window/window_settings.rs +++ b/examples/window/window_settings.rs @@ -41,7 +41,14 @@ fn main() { LogDiagnosticsPlugin::default(), FrameTimeDiagnosticsPlugin::default(), )) - .add_systems(Startup, (init_cursor_icons, init_window_icon)) + .add_systems( + Startup, + ( + init_cursor_icons, + #[cfg(feature = "custom_window_icon")] + init_window_icon + ) + ) .add_systems( Update, ( @@ -53,6 +60,8 @@ fn main() { cycle_cursor_icon, switch_level, make_visible, + #[cfg(all(feature = "bevy_asset", feature = "bevy_log"))] + log_asset_messages, ), ) .run(); @@ -77,7 +86,7 @@ fn toggle_vsync(input: Res>, mut window: Single<&mut Window } else { PresentMode::AutoVsync }; - #[cfg(feature="bevy_log")] + #[cfg(feature = "bevy_log")] info!("PRESENT_MODE: {:?}", window.present_mode); } } @@ -96,7 +105,7 @@ fn switch_level(input: Res>, mut window: Single<&mut Window WindowLevel::Normal => WindowLevel::AlwaysOnTop, WindowLevel::AlwaysOnTop => WindowLevel::AlwaysOnBottom, }; - #[cfg(feature="bevy_log")] + #[cfg(feature = "bevy_log")] info!("WINDOW_LEVEL: {:?}", window.window_level); } } @@ -176,19 +185,26 @@ fn init_cursor_icons( ])); } +#[cfg(feature = "custom_window_icon")] fn init_window_icon( mut commands: Commands, window: Single>, - #[cfg(feature = "custom_window_icon")] asset_server: Res, + asset_server: Res, ) { - #[cfg(feature = "custom_window_icon")] - { - use bevy::window::WindowIcon; + use bevy::window::WindowIcon; + + let icon_handle = asset_server.load("branding/icon.png"); + #[cfg(feature = "bevy_log")] + info!("icon_handle: {:?}", icon_handle); + commands.entity(*window).insert(WindowIcon { + handle: icon_handle, + }); +} - let icon_handle = asset_server.load("branding/icon.png"); - commands.entity(*window).insert(WindowIcon { - handle: icon_handle, - }); +#[cfg(all(feature = "bevy_asset", feature = "bevy_log"))] +fn log_asset_messages(mut asset_messages: MessageReader>) { + for msg in asset_messages.read() { + info!(?msg); } } From b988ef08790f2fe3c2232808891b6fb27c45c783 Mon Sep 17 00:00:00 2001 From: TeamDman Date: Sun, 28 Sep 2025 16:19:27 -0400 Subject: [PATCH 04/11] Run `cargo run -p build-templated-pages -- update features` --- docs/cargo_features.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/cargo_features.md b/docs/cargo_features.md index 2f4bbd95e5c0b..1926ae272b8b5 100644 --- a/docs/cargo_features.md +++ b/docs/cargo_features.md @@ -48,6 +48,7 @@ The default feature set enables most of the expected features of a game engine, |bevy_window|Windowing layer| |bevy_winit|winit window and input backend| |custom_cursor|Enable winit custom cursor support| +|custom_window_icon|Enable winit custom cursor support| |debug|Enable collecting debug information about systems and components to help with diagnostics| |default_font|Include a default font, containing only ASCII characters, at the cost of a 20kB binary size increase| |hdr|HDR image format support| From 80780fd1c8df9cbf7ba548aebd656fc31c9af877 Mon Sep 17 00:00:00 2001 From: TeamDman Date: Sun, 28 Sep 2025 21:05:40 -0400 Subject: [PATCH 05/11] Apply suggestions from code review Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- Cargo.toml | 2 +- crates/bevy_window/Cargo.toml | 2 +- docs/cargo_features.md | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 79f1f38d6f568..bc52c5718d48f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -564,7 +564,7 @@ reflect_auto_register_static = ["bevy_internal/reflect_auto_register_static"] # Enable winit custom cursor support custom_cursor = ["bevy_internal/custom_cursor"] -# Enable winit custom cursor support +# Enable winit custom window icon support custom_window_icon = ["bevy_internal/custom_window_icon"] # Experimental support for nodes that are ignored for UI layouting diff --git a/crates/bevy_window/Cargo.toml b/crates/bevy_window/Cargo.toml index fd52f393593f0..201c376784e0e 100644 --- a/crates/bevy_window/Cargo.toml +++ b/crates/bevy_window/Cargo.toml @@ -27,7 +27,7 @@ serialize = ["serde", "bevy_ecs/serialize", "bevy_input/serialize"] # Enable custom cursor support custom_cursor = ["bevy_image", "bevy_asset"] -# Enable custom icon support +# Enable custom window icon support custom_window_icon = ["bevy_image", "bevy_asset"] # Platform Compatibility diff --git a/docs/cargo_features.md b/docs/cargo_features.md index 1926ae272b8b5..dd7fb0c9a125f 100644 --- a/docs/cargo_features.md +++ b/docs/cargo_features.md @@ -48,7 +48,7 @@ The default feature set enables most of the expected features of a game engine, |bevy_window|Windowing layer| |bevy_winit|winit window and input backend| |custom_cursor|Enable winit custom cursor support| -|custom_window_icon|Enable winit custom cursor support| +|custom_window_icon|Enable winit custom window icon support| |debug|Enable collecting debug information about systems and components to help with diagnostics| |default_font|Include a default font, containing only ASCII characters, at the cost of a 20kB binary size increase| |hdr|HDR image format support| From fa87d64501a6b818a2db5ab1719046cff0f4c1c2 Mon Sep 17 00:00:00 2001 From: TeamDman Date: Sun, 28 Sep 2025 21:11:15 -0400 Subject: [PATCH 06/11] Remove `serialize` impl for WindowIcon since Handle isn't compatible --- crates/bevy_window/src/window.rs | 5 ----- 1 file changed, 5 deletions(-) diff --git a/crates/bevy_window/src/window.rs b/crates/bevy_window/src/window.rs index b9bd045a970f6..53a42f6c47846 100644 --- a/crates/bevy_window/src/window.rs +++ b/crates/bevy_window/src/window.rs @@ -788,11 +788,6 @@ impl Default for CursorOptions { derive(Reflect), reflect(Component, Debug, Default, Clone) )] -#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))] -#[cfg_attr( - all(feature = "serialize", feature = "bevy_reflect"), - reflect(Serialize, Deserialize) -)] #[cfg(feature = "custom_window_icon")] pub struct WindowIcon { /// Handle to the asset to be read into the window icon. From 8d4b1dea58ae064b9a4c45ec5c8e1cd344325c81 Mon Sep 17 00:00:00 2001 From: TeamDman Date: Mon, 29 Sep 2025 22:30:19 -0400 Subject: [PATCH 07/11] Initialize window icon on creation to fix problem problem with Changed system timing not working for late created windows --- crates/bevy_winit/src/lib.rs | 24 +++++ crates/bevy_winit/src/system.rs | 126 +++++++++++++++++++++++-- crates/bevy_winit/src/winit_windows.rs | 7 ++ examples/window/window_settings.rs | 35 ++++--- 4 files changed, 172 insertions(+), 20 deletions(-) diff --git a/crates/bevy_winit/src/lib.rs b/crates/bevy_winit/src/lib.rs index 94a9aa03ca63a..64484188c0354 100644 --- a/crates/bevy_winit/src/lib.rs +++ b/crates/bevy_winit/src/lib.rs @@ -208,6 +208,7 @@ impl AppSendEvent for Vec { } /// The parameters of the [`create_windows`] system. +#[cfg(not(feature = "custom_window_icon"))] pub type CreateWindowParams<'w, 's, F = ()> = ( Commands<'w, 's>, Query< @@ -227,5 +228,28 @@ pub type CreateWindowParams<'w, 's, F = ()> = ( Res<'w, WinitMonitors>, ); +/// The parameters of the [`create_windows`] system. +#[cfg(feature = "custom_window_icon")] +pub type CreateWindowParams<'w, 's, F = ()> = ( + Commands<'w, 's>, + Query< + 'w, + 's, + ( + Entity, + &'static mut Window, + &'static CursorOptions, + Option<&'static bevy_window::WindowIcon>, + Option<&'static RawHandleWrapperHolder>, + ), + F, + >, + MessageWriter<'w, WindowCreated>, + ResMut<'w, WinitActionRequestHandlers>, + Res<'w, AccessibilityRequested>, + Res<'w, WinitMonitors>, + Res<'w, bevy_asset::Assets>, // qualified path used to avoid unused import warnings +); + /// The parameters of the [`create_monitors`] system. pub type CreateMonitorParams<'w, 's> = (Commands<'w, 's>, ResMut<'w, WinitMonitors>); diff --git a/crates/bevy_winit/src/system.rs b/crates/bevy_winit/src/system.rs index 67155319e7689..3412df207e37e 100644 --- a/crates/bevy_winit/src/system.rs +++ b/crates/bevy_winit/src/system.rs @@ -56,29 +56,70 @@ use crate::{ /// default values. pub fn create_windows( event_loop: &ActiveEventLoop, - ( + params: SystemParamItem>, +) { + #[cfg(feature = "custom_window_icon")] + let ( mut commands, mut created_windows, mut window_created_events, mut handlers, accessibility_requested, monitors, - ): SystemParamItem>, -) { + assets, + ) = params; + #[cfg(not(feature = "custom_window_icon"))] + let ( + mut commands, + mut created_windows, + mut window_created_events, + mut handlers, + accessibility_requested, + monitors, + ) = params; + WINIT_WINDOWS.with_borrow_mut(|winit_windows| { ACCESS_KIT_ADAPTERS.with_borrow_mut(|adapters| { - for (entity, mut window, cursor_options, handle_holder) in &mut created_windows { + for entry in &mut created_windows { + #[cfg(feature = "custom_window_icon")] + let (entity, mut window, cursor_options, window_icon, handle_holder) = entry; + #[cfg(not(feature = "custom_window_icon"))] + let (entity, mut window, cursor_options, handle_holder) = entry; + if winit_windows.get_window(entity).is_some() { continue; } + + // warn!("Disabling window-icon-on-init for testing."); + // let window_icon: Option<&WindowIcon> = None; info!("Creating new window {} ({})", window.title.as_str(), entity); + #[cfg(feature = "custom_window_icon")] + let winit_window_icon = if let Some(window_icon) = window_icon { + if let Some(image) = assets.get(&window_icon.handle) { + get_winit_window_icon_from_bevy_image(image) + } else { + warn!( + ?entity, + ?window, + ?window_icon, + "Could not set window icon for window: image asset not found" + ); + None + } + } else { + None + }; + + let winit_window = winit_windows.create_window( event_loop, entity, &window, cursor_options, + #[cfg(feature = "custom_window_icon")] + winit_window_icon, adapters, &mut handlers, &accessibility_requested, @@ -93,14 +134,25 @@ pub fn create_windows( .resolution .set_scale_factor_and_apply_to_physical_size(winit_window.scale_factor() as f32); - commands.entity(entity).insert(( + let mut entity_commands = commands.entity(entity); + + entity_commands.insert(( CachedWindow(window.clone()), CachedCursorOptions(cursor_options.clone()), WinitWindowPressedKeys::default(), )); + #[cfg(feature = "custom_window_icon")] + { + if let Some(window_icon) = window_icon { + entity_commands.insert(CachedWindowIcon(window_icon.clone())); + } + } + + + if let Ok(handle_wrapper) = RawHandleWrapper::new(winit_window) { - commands.entity(entity).insert(handle_wrapper.clone()); + entity_commands.insert(handle_wrapper.clone()); if let Some(handle_holder) = handle_holder { *handle_holder.0.lock().unwrap() = Some(handle_wrapper); } @@ -296,6 +348,15 @@ pub(crate) struct CachedWindow(Window); #[derive(Debug, Clone, Component, Deref, DerefMut)] pub(crate) struct CachedCursorOptions(CursorOptions); +/// The cached state of the window icon so we can check which properties were changed from within the app. +/// Changed can fire before the window itself is added to [`WINIT_WINDOWS`] so we need to +/// apply the icon during window creation as well as when the icon changes. +/// Otherwise, we would need some kind of retry logic to apply the icon once the window is created, after the +/// changed message. At that point, it's more appropriate to just do it during creation. +#[cfg(feature = "custom_window_icon")] +#[derive(Debug, Clone, Component, Deref, DerefMut)] +pub(crate) struct CachedWindowIcon(WindowIcon); + /// Propagates changes from [`Window`] entities to the [`winit`] backend. /// /// # Notes @@ -624,16 +685,62 @@ pub(crate) fn changed_cursor_options( }); } +pub(crate) fn get_winit_window_icon_from_bevy_image(image: &Image) -> Option { + // Convert to rgba image + let rgba_image = match image.clone().try_into_dynamic() { + Ok(dynamic_image) => { + // winit icon expects 32bpp RGBA data + dynamic_image.into_rgba8() + } + Err(error) => { + error!( + ?image, + ?error, + "Could not get window icon: failed to convert image to RGBA", + ); + return None; + } + }; + + // Convert to winit image + let width = rgba_image.width(); + let height = rgba_image.height(); + match winit::window::Icon::from_rgba(rgba_image.into_raw(), width, height) { + Ok(icon) => Some(icon), + Err(error) => { + error!( + ?image, + ?error, + "Could not get window icon: failed to construct winit window icon from RGBA buffer", + ); + None + } + } +} + #[cfg(feature = "custom_window_icon")] pub(crate) fn changed_window_icon( - changed_windows: Query<(Entity, &Window, &WindowIcon), Changed>, + mut commands: bevy_ecs::system::Commands, + changed_windows: Query<(Entity, &Window, &WindowIcon, Option<&CachedWindowIcon>), Changed>, assets: Res>, _non_send_marker: NonSendMarker, ) { WINIT_WINDOWS.with_borrow(|winit_windows| { - for (window_entity, window, window_icon) in changed_windows { + for (window_entity, window, window_icon, cached_icon) in changed_windows { + // Skip if no work to do + if let Some(cached_icon) = cached_icon + && cached_icon.0.handle == window_icon.handle { + continue; + } + // Identify window let Some(winit_window) = winit_windows.get_window(window_entity) else { + tracing::debug!( + ?window_entity, + ?window, + ?window_icon, + "Could not set window icon for window: winit window not found. Assuming that the winit window is not yet created, so the icon will instead be applied during window creation." + ); continue; }; @@ -698,6 +805,9 @@ pub(crate) fn changed_window_icon( "Setting window icon" ); winit_window.set_window_icon(Some(icon)); + + // Update the cached icon + commands.entity(window_entity).insert(CachedWindowIcon(window_icon.clone())); } }); } diff --git a/crates/bevy_winit/src/winit_windows.rs b/crates/bevy_winit/src/winit_windows.rs index 40151845c46f9..963c217c98523 100644 --- a/crates/bevy_winit/src/winit_windows.rs +++ b/crates/bevy_winit/src/winit_windows.rs @@ -59,6 +59,7 @@ impl WinitWindows { entity: Entity, window: &Window, cursor_options: &CursorOptions, + #[cfg(feature = "custom_window_icon")] window_icon: Option, adapters: &mut AccessKitAdapters, handlers: &mut WinitActionRequestHandlers, accessibility_requested: &AccessibilityRequested, @@ -333,6 +334,12 @@ impl WinitWindows { ); } + // Set window icon if provided + #[cfg(feature = "custom_window_icon")] + if let Some(icon) = window_icon { + winit_window.set_window_icon(Some(icon)); + } + self.entity_to_winit.insert(entity, winit_window.id()); self.winit_to_entity.insert(winit_window.id(), entity); diff --git a/examples/window/window_settings.rs b/examples/window/window_settings.rs index 33d2c30bc53ff..06042ac60dd60 100644 --- a/examples/window/window_settings.rs +++ b/examples/window/window_settings.rs @@ -7,8 +7,7 @@ use bevy::{ diagnostic::{FrameCount, FrameTimeDiagnosticsPlugin, LogDiagnosticsPlugin}, prelude::*, window::{ - CursorGrabMode, CursorIcon, CursorOptions, PresentMode, SystemCursorIcon, WindowLevel, - WindowTheme, + CursorGrabMode, CursorIcon, CursorOptions, PresentMode, PrimaryWindow, SystemCursorIcon, WindowLevel, WindowTheme }, }; @@ -46,8 +45,8 @@ fn main() { ( init_cursor_icons, #[cfg(feature = "custom_window_icon")] - init_window_icon - ) + init_window_icon, + ), ) .add_systems( Update, @@ -67,7 +66,7 @@ fn main() { .run(); } -fn make_visible(mut window: Single<&mut Window>, frames: Res) { +fn make_visible(mut window: Single<&mut Window, With>, frames: Res) { // The delay may be different for your app or system. if frames.0 == 3 { // At this point the gpu is ready to show the app so we can make the window visible. @@ -79,7 +78,7 @@ fn make_visible(mut window: Single<&mut Window>, frames: Res) { /// This system toggles the vsync mode when pressing the button V. /// You'll see fps increase displayed in the console. -fn toggle_vsync(input: Res>, mut window: Single<&mut Window>) { +fn toggle_vsync(input: Res>, mut window: Single<&mut Window, With>) { if input.just_pressed(KeyCode::KeyV) { window.present_mode = if matches!(window.present_mode, PresentMode::AutoVsync) { PresentMode::AutoNoVsync @@ -98,7 +97,7 @@ fn toggle_vsync(input: Res>, mut window: Single<&mut Window /// This feature only works on some platforms. Please check the /// [documentation](https://docs.rs/bevy/latest/bevy/prelude/struct.Window.html#structfield.window_level) /// for more details. -fn switch_level(input: Res>, mut window: Single<&mut Window>) { +fn switch_level(input: Res>, mut window: Single<&mut Window, With>) { if input.just_pressed(KeyCode::KeyT) { window.window_level = match window.window_level { WindowLevel::AlwaysOnBottom => WindowLevel::Normal, @@ -115,7 +114,7 @@ fn switch_level(input: Res>, mut window: Single<&mut Window /// This feature only works on some platforms. Please check the /// [documentation](https://docs.rs/bevy/latest/bevy/prelude/struct.Window.html#structfield.enabled_buttons) /// for more details. -fn toggle_window_controls(input: Res>, mut window: Single<&mut Window>) { +fn toggle_window_controls(input: Res>, mut window: Single<&mut Window, With>) { let toggle_minimize = input.just_pressed(KeyCode::Digit1); let toggle_maximize = input.just_pressed(KeyCode::Digit2); let toggle_close = input.just_pressed(KeyCode::Digit3); @@ -134,7 +133,7 @@ fn toggle_window_controls(input: Res>, mut window: Single<& } /// This system will then change the title during execution -fn change_title(mut window: Single<&mut Window>, time: Res