Skip to content

Conversation

TeamDman
Copy link

@TeamDman TeamDman commented Sep 28, 2025

Objective

  • Make it easy to set the icon for the window using existing Bevy asset pipeline.

Related:

Solution

  • Added a WindowIcon { handle: Handle<Image> } component that can be added to entities with the Window component.

Testing

It working:

cargo run --example window_settings

image

It disabled:

cargo run --example window_settings --no-default-features --features bevy_window,bevy_winit,bevy_log

image

Tested on Windows 10.


Showcase

fn init_window_icon(
    mut commands: Commands,
    window: Single<Entity, With<Window>>,
    asset_server: Res<AssetServer>,
) {
    use bevy::window::WindowIcon;

    let icon_handle = asset_server.load("branding/icon.png");
    commands.entity(*window).insert(WindowIcon {
        handle: icon_handle,
    });
}

Caveats

It works with the default features, but trying to manually enable them it is not working lol

❯ cargo run --example window_settings --no-default-features --features bevy_window,bevy_winit,custom_window_icon,bevy_log,bevy_asset,bevy_image,asset_processor,bevy_render
   Compiling bevy v0.17.0-rc.2 (D:\Repos\Games\bevy)
    Finished `dev` profile [unoptimized + debuginfo] target(s) in 13.14s
     Running `target\debug\examples\window_settings.exe`
2025-09-28T16:37:47.178249Z  INFO bevy_render::renderer: AdapterInfo { name: "NVIDIA GeForce RTX 4090", vendor: 4318, device: 9860, device_type: DiscreteGpu, driver: "NVIDIA", driver_info: "580.97", backend: Vulkan }
2025-09-28T16:37:47.579403Z  INFO bevy_render::batching::gpu_preprocessing: GPU preprocessing is fully supported on this device.
2025-09-28T16:37:47.602729Z  INFO bevy_winit::system: Creating new window I am a window! (0v0)
2025-09-28T16:37:47.623076Z  WARN bevy_winit::system: Could not set window icon for window Seconds since startup: 0: image asset not found window_icon.handle=StrongHandle<Image>{ id: Index(AssetIndex { generation: 0, index: 0 }), path: Some(branding/icon.png) }

I suspect this is either because:

  1. I haven't enabled a feature required for the asset to actually get loaded
  2. A race condition where the asset is always loaded in time when all features are enabled, but otherwise the Changed system is running before the asset is finished loading

Both scenarios should be an easy fix. I will proceed with investigating by adding a system to print AssetEvent messages to see what's going on

Additionally, this is currently using the Changed event to detect added/modified component, and is not setting the icon when the window is created if the component is present. This means that there is a small delay between the window's creation and the first setting of the icon, but in practice it should be unnoticeable :P

Copy link
Contributor

Welcome, new contributor!

Please make sure you've read our contributing guide and we look forward to reviewing your pull request shortly ✨

@TeamDman TeamDman changed the title Implement custom_window_icon feature Introduce WindowIcon component Sep 28, 2025
@alice-i-cecile alice-i-cecile added C-Feature A new feature, making something new possible M-Needs-Release-Note Work that should be called out in the blog due to impact X-Contentious There are nontrivial implications that should be thought through S-Needs-Review Needs reviewer attention (from anyone!) to move forward A-Windowing Platform-agnostic interface layer to run your app in labels Sep 28, 2025
Copy link
Contributor

It looks like your PR has been selected for a highlight in the next release blog post, but you didn't provide a release note.

Please review the instructions for writing release notes, then expand or revise the content in the release notes directory to showcase your changes.

@alice-i-cecile alice-i-cecile added S-Waiting-on-Author The author needs to make changes or address concerns before this can be merged and removed S-Needs-Review Needs reviewer attention (from anyone!) to move forward labels Sep 28, 2025
@JMS55
Copy link
Contributor

JMS55 commented Sep 28, 2025

Hmm, usually you want to:

  • Windows: Embed an ico into the exe itself using something like https://github.com/nabijaczleweli/rust-embed-resource (or non-Rust tooling)
  • Linux: Set an svg in the .desktop file you ship with your app
  • Web: Set an svg in your html header
  • macOS and mobile: I'm not familiar

Doing it properly this way gives you better integration with things like app stores and app launchers.

I'm not against providing this API, but we should note that it's not the best way to do things.

@alice-i-cecile
Copy link
Member

@mockersf has said effectively the same thing. At the least, I think we should clearly document how to do this effectively.

Long-term, I would be interested in integrating this with tooling to create per-platform executables correctly, probably with bevy_cli.

@alice-i-cecile alice-i-cecile added X-Controversial There is active debate or serious implications around merging this PR and removed X-Contentious There are nontrivial implications that should be thought through labels Sep 28, 2025
@TeamDman
Copy link
Author

TeamDman commented Sep 28, 2025

I've identified this as a working command using --no-default-features, the required features was non-obvious to me

Happy path:

cargo run --package bevy --example window_settings --no-default-features --features bevy_window,bevy_winit,custom_window_icon,bevy_log,png,bevy_image,async_executor,asset_processor,bevy_asset,bevy_render

RUST_LOG="debug,naga=info,offset_allocator=info,bevy_shader=info,bevy_app=info,wgpu_hal=warn"

2025-09-28T19:56:17.816273Z DEBUG bevy_asset::io::file: Asset Server using D:\Repos\Games\bevy\assets as its base path.
2025-09-28T19:56:18.543656Z  INFO bevy_winit::system: Creating new window I am a window! (0v0)
2025-09-28T19:56:18.543979Z DEBUG bevy_winit::winit_windows: Display information:  Window physical resolution: 500x300  Window logical resolution: 500x300  Monitor name:   Scale factor: 0  Refresh rate (Hz): 0.000
2025-09-28T19:56:18.562441Z  INFO window_settings: icon_handle: StrongHandle<Image>{ id: Index(AssetIndex { generation: 0, index: 0 }), path: Some(branding/icon.png) }
2025-09-28T19:56:18.567022Z  INFO window_settings: msg=LoadedWithDependencies { id: AssetId<bevy_image::image::Image>{ index: 0, generation: 0} }
2025-09-28T19:56:18.572561Z DEBUG bevy_winit::system: Setting window icon window_entity=0v0 window.title="Seconds since startup: 0" window_icon.handle=StrongHandle<Image>{ id: Index(AssetIndex { generation: 0, index: 0 }), path: Some(branding/icon.png) } image_size=UVec2(256, 256)
2025-09-28T19:56:18.599191Z  INFO window_settings: msg=Added { id: AssetId<bevy_image::image::Image>{uuid: 97128bb1-2588-480b-bdc6-87b4adbec477} }
2025-09-28T19:56:18.599302Z  INFO window_settings: msg=Added { id: AssetId<bevy_image::image::Image>{uuid: d18ad97e-a322-4981-9505-44c59a4b5e46} }
2025-09-28T19:56:18.599396Z  INFO window_settings: msg=Added { id: AssetId<bevy_image::image::Image>{ index: 0, generation: 0} }

Problem path when not using all the necessary features; the asset never loads:

run --package bevy --example window_settings --no-default-features --features bevy_window,bevy_winit,custom_window_icon,bevy_log,bevy_asset

2025-09-28T20:06:16.122437Z DEBUG bevy_asset::io::file: Asset Server using D:\Repos\Games\bevy\assets as its base path.
2025-09-28T20:06:16.152055Z  INFO bevy_winit::system: Creating new window I am a window! (0v0)
2025-09-28T20:06:16.152258Z DEBUG bevy_winit::winit_windows: Display information:  Window physical resolution: 500x300  Window logical resolution: 500x300  Monitor name:   Scale factor: 0  Refresh rate (Hz): 0.000
2025-09-28T20:06:16.164915Z  INFO window_settings: icon_handle: StrongHandle<Image>{ id: Index(AssetIndex { generation: 0, index: 0 }), path: Some(branding/icon.png) }
2025-09-28T20:06:16.170763Z  WARN bevy_winit::system: Could not set window icon for window: image asset not found window_entity=0v0 window=Window { present_mode: AutoVsync, mode: Windowed, position: Automatic, resolution: WindowResolution { physical_width: 500, physical_height: 300, scale_factor_override: None, scale_factor: 1.0 }, title: "Seconds since startup: 0", name: Some("bevy.app"), composite_alpha_mode: Auto, resize_constraints: WindowResizeConstraints { min_width: 180.0, min_height: 120.0, max_width: inf, max_height: inf }, resizable: true, enabled_buttons: EnabledButtons { minimize: true, maximize: false, close: true }, decorations: true, transparent: false, focused: true, window_level: Normal, canvas: None, fit_canvas_to_parent: true, prevent_default_event_handling: false, internal: InternalWindowState { minimize_request: None, maximize_request: None, drag_move_request: false, drag_resize_request: None, physical_cursor_position: None }, ime_enabled: false, ime_position: Vec2(0.0, 0.0), window_theme: Some(Dark), visible: false, skip_taskbar: false, clip_children: true, desired_maximum_frame_latency: None, recognize_pinch_gesture: false, recognize_rotation_gesture: false, recognize_doubletap_gesture: false, recognize_pan_gesture: None, movable_by_window_background: false, fullsize_content_view: false, has_shadow: true, titlebar_shown: true, titlebar_transparent: false, titlebar_show_title: true, titlebar_show_buttons: true, prefers_home_indicator_hidden: false, prefers_status_bar_hidden: false, preferred_screen_edges_deferring_system_gestures: None } window_icon=WindowIcon { handle: StrongHandle<Image>{ id: Index(AssetIndex { generation: 0, index: 0 }), path: Some(branding/icon.png) } }
2025-09-28T20:06:16.171429Z  INFO window_settings: msg=Added { id: AssetId<bevy_image::image::Image>{uuid: 97128bb1-2588-480b-bdc6-87b4adbec477} }
2025-09-28T20:06:16.171538Z  INFO window_settings: msg=Added { id: AssetId<bevy_image::image::Image>{uuid: d18ad97e-a322-4981-9505-44c59a4b5e46} }

without png or the other features, the asset event LoadedWithDependencies is never emitted

Copy link
Contributor

You added a new feature but didn't update the readme. Please run cargo run -p build-templated-pages -- update features to update it, and commit the file change.

@TeamDman
Copy link
Author

TeamDman commented Sep 28, 2025

Here's a summary of how I do exe icon setup for my Windows stuff usually

Sample repo using Bevy in addition to some Windows-rs things to get a system tray icon: https://github.com/teamdman/youre-muted-btw

  1. Convert icon.png into a .ico file and create a resized texture asset by creating a make-icon.ps1 file
# create .ico
magick convert -background transparent "icon.png" -define icon:auto-resize=16,24,32,48,64,72,96,128,256 "favicon.ico"

# create resized image
magick convert -background transparent "icon.png" -resize 64x64 ".\assets\textures\icon.png"
  1. Add build dependencies to Cargo.toml
[build-dependencies]
embed-resource = "3.0.3"
  1. Create an icons.rc file
aaa_my_icon ICON "favicon.ico"

I use aaa_my_icon to make it very obvious what part of this stuff is my user-specified identifier

  1. Create a build.rs file
extern crate embed_resource;
fn main() {
    println!("cargo:rerun-if-changed=icons.rc");
    println!("cargo:rerun-if-changed=favicon.ico");
    embed_resource::compile("icons.rc", embed_resource::NONE)
        .manifest_required()
        .unwrap();
}

This should result in the exe having the correct icon

image

Additionally, we can retrieve the icon from the exe as an HICON

https://github.com/TeamDman/youre-muted-btw/blob/4783e1efe184083b053882cc066c9932588551c5/crates/tray/src/lib.rs#L299-L319

        // Load the icon from embedded resources using LoadIconW
        let icon = {
            let instance = GetModuleHandleW(None)?;
            let resource_name = w!("aaa_my_icon");
            match LoadIconW(Some(HINSTANCE(instance.0)), resource_name) {
                Ok(hicon) => hicon,
                Err(e) => {
                    error!("Failed to load icon resource 'aaa_my_icon': {e}");
                    // Fallback to default application icon
                    match LoadIconW(None, IDI_APPLICATION) {
                        Ok(fallback_icon) => fallback_icon,
                        Err(fallback_error) => {
                            error!(
                                "Failed to load fallback IDI_APPLICATION icon: {fallback_error}"
                            );
                            return Err(fallback_error.into());
                        }
                    }
                }
            }
        };

HICON can be converted to more friendly image types; see https://stackoverflow.com/a/78190249/11141271

I have in the past successfully converted HICONs into images and added them as textures for the asset system

https://github.com/TeamDman/Cursor-Hero/blob/9ba3be8c66d468f0d3e54da5e6b1b4cd0e7718e5/crates/winutils/examples/app_icons_bevy_example.rs

fn receive(
    mut commands: Commands,
    mut bridge: EventReader<GameboundMessage>,
    mut icons_so_far: Local<usize>,
    mut textures: ResMut<Assets<Image>>,
) {
    for msg in bridge.read() {
        match msg {
            GameboundMessage::RunningProcessIcons(icons) => {
                info!("Received icons: {:?}", icons.len());
                for (exe_path, images) in icons {
                    for image in images {
                        debug!("{}x{}", image.width(), image.height());
                        let dynamic = DynamicImage::ImageRgba8(image.clone());
                        let handle = textures.add(Image::from_dynamic(dynamic, true));
...

However I haven't gone as far as generalizing the .rc assets from the current binary to become available

At the time it was easier to just have my make-icon.ps1 file copy to the necessary locations since I only had the one asset for the icon.

@mockersf
Copy link
Member

This PR is for the window icon, not the application icon. it's something that exists only on windows and on x11, see https://docs.rs/winit/latest/winit/window/struct.Window.html#method.set_window_icon

@mockersf mockersf removed the M-Needs-Release-Note Work that should be called out in the blog due to impact label Sep 28, 2025
@TeamDman
Copy link
Author

Correct, just wanted to summarize my own experience regarding exe icons as it was mentioned

@TeamDman TeamDman marked this pull request as ready for review September 29, 2025 01:00
@Copilot Copilot AI review requested due to automatic review settings September 29, 2025 01:00
Copy link

@Copilot Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull Request Overview

This PR introduces a WindowIcon component to enable setting window icons through Bevy's existing asset pipeline. The implementation provides a component-based approach where users can attach an image asset handle to a window entity to customize its icon.

  • Adds a new WindowIcon component that holds a Handle<Image> for the window icon
  • Implements a system to monitor changes to the WindowIcon component and update the window icon accordingly
  • Adds a new custom_window_icon feature flag to control the availability of this functionality

Reviewed Changes

Copilot reviewed 9 out of 9 changed files in this pull request and generated 3 comments.

Show a summary per file
File Description
examples/window/window_settings.rs Adds example usage of the new WindowIcon component with feature-gated code
docs/cargo_features.md Documents the new custom_window_icon feature
crates/bevy_winit/src/system.rs Implements the core system that handles WindowIcon changes and sets the winit window icon
crates/bevy_winit/src/lib.rs Registers the new window icon system with the plugin
crates/bevy_winit/Cargo.toml Adds dependencies and feature configuration for custom_window_icon
crates/bevy_window/src/window.rs Defines the WindowIcon component struct
crates/bevy_window/Cargo.toml Adds feature configuration for custom_window_icon
crates/bevy_internal/Cargo.toml Propagates the custom_window_icon feature flag
Cargo.toml Adds custom_window_icon to default features and example requirements

Tip: Customize your code reviews with copilot-instructions.md. Create the file or learn how to get started.

@TeamDman
Copy link
Author

TeamDman commented Sep 30, 2025

Problem identified:

I had relied on Changed firing for the WindowIcon component being present on new entities, which it does.
However, even though it fires in Last, it still fires BEFORE the window is available in WINIT_WINDOWS

2025-09-30T01:54:58.703034Z  INFO bevy_winit::system: Creating new window I am a window! (0v0)
2025-09-30T01:54:58.713488Z  INFO window_settings: icon_handle: StrongHandle<Image>{ id: Index(AssetIndex { generation: 0, index: 7 }), path: Some(branding/icon.png) }
2025-09-30T01:54:58.729812Z  INFO window_settings: msg=LoadedWithDependencies { id: AssetId<bevy_image::image::Image>{ index: 5, generation: 0} }
2025-09-30T01:54:58.730128Z  INFO window_settings: msg=LoadedWithDependencies { id: AssetId<bevy_image::image::Image>{ index: 4, generation: 0} }
2025-09-30T01:54:58.730380Z  INFO window_settings: msg=LoadedWithDependencies { id: AssetId<bevy_image::image::Image>{ index: 8, generation: 0} }
2025-09-30T01:54:58.730602Z  INFO window_settings: msg=LoadedWithDependencies { id: AssetId<bevy_image::image::Image>{ index: 7, generation: 0} }
2025-09-30T01:54:58.771542Z  WARN bevy_winit::system: Could not set window icon for window: winit window not found window_entity=14v0 window=Window { present_mode: Fifo, mode: Windowed, position: Automatic, resolution: WindowResolution { physical_width: 300, physical_height: 200, scale_factor_override: None, scale_factor: 1.0 }, title: "I am another window!", name: None, composite_alpha_mode: Auto, resize_constraints: WindowResizeConstraints { min_width: 180.0, min_height: 120.0, max_width: inf, max_height: inf }, resizable: true, enabled_buttons: EnabledButtons { minimize: true, maximize: true, close: true }, decorations: true, transparent: false, focused: true, window_level: Normal, canvas: None, fit_canvas_to_parent: false, prevent_default_event_handling: true, internal: InternalWindowState { minimize_request: None, maximize_request: None, drag_move_request: false, drag_resize_request: None, physical_cursor_position: None }, ime_enabled: false, ime_position: Vec2(0.0, 0.0), window_theme: None, visible: true, skip_taskbar: false, clip_children: true, desired_maximum_frame_latency: None, recognize_pinch_gesture: false, recognize_rotation_gesture: false, recognize_doubletap_gesture: false, recognize_pan_gesture: None, movable_by_window_background: false, fullsize_content_view: false, has_shadow: true, titlebar_shown: true, titlebar_transparent: false, titlebar_show_title: true, titlebar_show_buttons: true, prefers_home_indicator_hidden: false, prefers_status_bar_hidden: false, preferred_screen_edges_deferring_system_gestures: None } window_icon=WindowIcon { handle: StrongHandle<Image>{ id: Index(AssetIndex { generation: 0, index: 8 }), path: Some(textures/rpg/props/generic-rpg-tree02.png) } }
2025-09-30T01:54:58.819031Z  WARN bevy_winit::system: Disabling window-icon-on-init for testing.
2025-09-30T01:54:58.819302Z  INFO bevy_winit::system: Creating new window I am another window! (14v0)

Note that the Could not set window icon for window is firing before Creating new window I am another window!, leaving the second window without its custom icon.

image
sequenceDiagram
    participant World
    participant System
    participant Winit
    
    World->>World: Spawn Entity with Window and WindowIcon
    System->>World: Query Changed<WindowIcon>
    System->>Winit: Look for winit window
    Winit-->>System: ❌ Not found
    Note over System: Failure: winit window<br/>doesn't exist yet
    Winit->>Winit: Create winit window
Loading

I previously solved this by having the system using Changed fire an event, and having the event reading system re-send the event if the winit window is not found, knowing that it will soon appear.

This approach worked in the other project, but for this PR it sounds more reasonable to simply set the icon during window construction and have an internal CachedWindowIcon component to prevent duplicate work in the Changed system.

This is working, supporting windows beyond just the first PrimaryWindow

sequenceDiagram
    participant World
    participant System
    participant Winit
    
    World->>World: Spawn Entity with Window and WindowIcon
    System->>World: Query Changed<WindowIcon>
    System->>Winit: Look for winit window
    Winit-->>System: ❌ Not found
    Note over System: Debug log: assume<br/>not ready yet
    Winit->>Winit: Create winit window
    rect rgb(50, 155, 100)
    Winit->>Winit: Set icon ✓
    end
Loading
image

@TeamDman TeamDman requested a review from Copilot September 30, 2025 03:00
Copy link

@Copilot Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull Request Overview

Copilot reviewed 11 out of 11 changed files in this pull request and generated 6 comments.


Tip: Customize your code reviews with copilot-instructions.md. Create the file or learn how to get started.

@TeamDman
Copy link
Author

In my real application I'm now encountering the race condition where the window is created before the asset is ready.
Looks like it's going to be necessary to listen to add a system using MessageReader<AssetEvent<Image>> to trigger icon refresh.

My logs
teamy-mft on  HEAD (230b685) [!?] is 📦 v0.3.0 via 🦀 v1.91.0-nightly took 4m13s
 cargo run -- engine run --debug
    Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.90s
     Running `target\debug\teamy-mft.exe engine run --debug`
  DEBUG teamy_mft: Tracing initialized with level: Level(Debug)
  DEBUG teamy_mft::engine::run: Building Bevy engine
   INFO bevy_diagnostic::system_information_diagnostics_plugin::internal: SystemInfo { os: "Windows 10 Pro", kernel: "19045", cpu: "AMD Ryzen 9 5950X 16-Core Processor", core_count: "16", memory: "63.9 GiB" }
  DEBUG bevy_asset::io::file: Asset Server using G:\Programming\Repos\teamy-mft\assets as its base path.
   INFO bevy_render::renderer: AdapterInfo { name: "NVIDIA GeForce RTX 4090", vendor: 4318, device: 9860, device_type: DiscreteGpu, driver: "NVIDIA", driver_info: "580.97", backend: Vulkan }
  DEBUG teamy_mft::engine::run: Bevy engine built
   INFO teamy_mft::engine::run: Running Bevy engine
   INFO bevy_render::batching::gpu_preprocessing: GPU preprocessing is fully supported on this device.
  DEBUG teamy_mft::engine::sync_dir_plugin: [DISABLED] Emitted ReadSyncDirectory event on startup
   INFO teamy_mft::engine::mft_file_overview_window_plugin: Spawned MFT overview window, title: "MFT Files - Overview"
  DEBUG teamy_mft::engine::assets::asset_message_log_plugin: Image asset event, msg: LoadedWithDependencies { id: AssetId<bevy_image::image::Image>{ index: 5, generation: 0} }
  DEBUG teamy_mft::engine::assets::asset_message_log_plugin: Image asset event, msg: LoadedWithDependencies { id: AssetId<bevy_image::image::Image>{ index: 4, generation: 0} }
  DEBUG bevy_winit::system: 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., window_entity: 13v0, window: Window { present_mode: Fifo, mode: Windowed, position: Automatic, resolution: WindowResolution { physical_width: 1280, physical_height: 720, scale_factor_override: None, scale_factor: 1.0 }, title: "MFT Files - Overview", name: None, composite_alpha_mode: Auto, resize_constraints: WindowResizeConstraints { min_width: 180.0, min_height: 120.0, max_width: inf, max_height: inf }, resizable: true, enabled_buttons: EnabledButtons { minimize: true, maximize: true, close: true }, decorations: true, transparent: false, focused: true, window_level: Normal, canvas: None, fit_canvas_to_parent: false, prevent_default_event_handling: true, internal: InternalWindowState { minimize_request: None, maximize_request: None, drag_move_request: false, drag_resize_request: None, physical_cursor_position: None }, ime_enabled: false, ime_position: Vec2(0.0, 0.0), window_theme: None, visible: true, skip_taskbar: false, clip_children: true, desired_maximum_frame_latency: None, recognize_pinch_gesture: false, recognize_rotation_gesture: false, recognize_doubletap_gesture: false, recognize_pan_gesture: None, movable_by_window_background: false, fullsize_content_view: false, has_shadow: true, titlebar_shown: true, titlebar_transparent: false, titlebar_show_title: true, titlebar_show_buttons: true, prefers_home_indicator_hidden: false, prefers_status_bar_hidden: false, preferred_screen_edges_deferring_system_gestures: None }, window_icon: WindowIcon { handle: StrongHandle<Image>{ id: Index(AssetIndex { generation: 0, index: 7 }), path: Some(textures/icon.png) } }
   INFO bevy_winit::system: Creating new window MFT Files - Overview (13v0)
   WARN bevy_winit::winit_window_icon: Could not create winit window icon from Bevy assets: image asset not found, image_handle: StrongHandle<Image>{ id: Index(AssetIndex { generation: 0, index: 7 }), path: Some(textures/icon.png) }
   WARN bevy_winit::system: Could not set window icon for window: failed to acquire winit window icon, entity: 13v0, window: Mut(Window { present_mode: Fifo, mode: Windowed, position: Automatic, resolution: WindowResolution { physical_width: 1280, physical_height: 720, scale_factor_override: None, scale_factor: 1.0 }, title: "MFT Files - Overview", name: None, composite_alpha_mode: Auto, resize_constraints: WindowResizeConstraints { min_width: 180.0, min_height: 120.0, max_width: inf, max_height: inf }, resizable: true, enabled_buttons: EnabledButtons { minimize: true, maximize: true, close: true }, decorations: true, transparent: false, focused: true, window_level: Normal, canvas: None, fit_canvas_to_parent: false, prevent_default_event_handling: true, internal: InternalWindowState { minimize_request: None, maximize_request: None, drag_move_request: false, drag_resize_request: None, physical_cursor_position: None }, ime_enabled: false, ime_position: Vec2(0.0, 0.0), window_theme: None, visible: true, skip_taskbar: false, clip_children: true, desired_maximum_frame_latency: None, recognize_pinch_gesture: false, recognize_rotation_gesture: false, recognize_doubletap_gesture: false, recognize_pan_gesture: None, movable_by_window_background: false, fullsize_content_view: false, has_shadow: true, titlebar_shown: true, titlebar_transparent: false, titlebar_show_title: true, titlebar_show_buttons: true, prefers_home_indicator_hidden: false, prefers_status_bar_hidden: false, preferred_screen_edges_deferring_system_gestures: None }), window_icon: WindowIcon { handle: StrongHandle<Image>{ id: Index(AssetIndex { generation: 0, index: 7 }), path: Some(textures/icon.png) } }
  DEBUG bevy_winit::winit_windows: Display information:  Window physical resolution: 1280x720  Window logical resolution: 1280x720  Monitor name:   Scale factor: 0  Refresh rate (Hz): 0.000
  DEBUG teamy_mft::engine::assets::asset_message_log_plugin: Image asset event, msg: Added { id: AssetId<bevy_image::image::Image>{uuid: 97128bb1-2588-480b-bdc6-87b4adbec477} }
  DEBUG teamy_mft::engine::assets::asset_message_log_plugin: Image asset event, msg: Added { id: AssetId<bevy_image::image::Image>{uuid: d18ad97e-a322-4981-9505-44c59a4b5e46} }
  DEBUG teamy_mft::engine::assets::asset_message_log_plugin: Image asset event, msg: Added { id: AssetId<bevy_image::image::Image>{ index: 0, generation: 0} }
  DEBUG teamy_mft::engine::assets::asset_message_log_plugin: Image asset event, msg: Added { id: AssetId<bevy_image::image::Image>{ index: 1, generation: 0} }
  DEBUG teamy_mft::engine::assets::asset_message_log_plugin: Image asset event, msg: Added { id: AssetId<bevy_image::image::Image>{ index: 2, generation: 0} }
  DEBUG teamy_mft::engine::assets::asset_message_log_plugin: Image asset event, msg: Added { id: AssetId<bevy_image::image::Image>{ index: 3, generation: 0} }
  DEBUG teamy_mft::engine::assets::asset_message_log_plugin: Image asset event, msg: Added { id: AssetId<bevy_image::image::Image>{ index: 6, generation: 0} }
  DEBUG teamy_mft::engine::assets::asset_message_log_plugin: Image asset event, msg: Added { id: AssetId<bevy_image::image::Image>{ index: 5, generation: 0} }
  DEBUG teamy_mft::engine::assets::asset_message_log_plugin: Image asset event, msg: Added { id: AssetId<bevy_image::image::Image>{ index: 4, generation: 0} }
  DEBUG teamy_mft::engine::assets::asset_message_log_plugin: Image asset event, msg: LoadedWithDependencies { id: AssetId<bevy_image::image::Image>{ index: 7, generation: 0} }
  DEBUG teamy_mft::engine::assets::asset_message_log_plugin: Image asset event, msg: Removed { id: AssetId<bevy_image::image::Image>{ index: 6, generation: 0} }
  DEBUG teamy_mft::engine::assets::asset_message_log_plugin: Image asset event, msg: Removed { id: AssetId<bevy_image::image::Image>{ index: 3, generation: 0} }
  DEBUG teamy_mft::engine::assets::asset_message_log_plugin: Image asset event, msg: Added { id: AssetId<bevy_image::image::Image>{ index: 7, generation: 0} }
  DEBUG bevy_time::virt: delta time larger than maximum delta, clamping delta to 250ms and skipping 402.8105ms
   INFO bevy_winit::system: Closing window 13v0

Defined marker component for when winit icon update should be attemped.
Insert marker component when assets are changed or made ready.
@TeamDman
Copy link
Author

Added asset change detection to fix race condition where winit icon is attempted to be set from the asset before the asset or window is ready.

If the winit window is not present yet, it will attempt to set the icon when the window is created.
If the window is created when the asset is not ready, it will attempt to set the icon when the asset becomes ready.
If the WindowIcon::handle changes it will also update the winit window icon.
When the asset becomes ready or changes it will also update the winit window icon.

Now works for my real use case :D

image
Logs from my app demonstrating the asset becoming ready after the window is created
  DEBUG teamy_mft::engine::assets::asset_message_log_plugin: Image asset event, msg: LoadedWithDependencies { id: AssetId<bevy_image::image::Image>{ index: 5, generation: 0} }
  DEBUG teamy_mft::engine::assets::asset_message_log_plugin: Image asset event, msg: LoadedWithDependencies { id: AssetId<bevy_image::image::Image>{ index: 4, generation: 0} }
  DEBUG bevy_winit::system: Could not set winit window icon: winit window not found - will try again later during window creation, window_entity: 13v0, window: Window { present_mode: Fifo, mode: Windowed, position: Automatic, resolution: WindowResolution { physical_width: 1280, physical_height: 720, scale_factor_override: None, scale_factor: 1.0 }, title: "MFT Files - Overview", name: None, composite_alpha_mode: Auto, resize_constraints: WindowResizeConstraints { min_width: 180.0, min_height: 120.0, max_width: inf, max_height: inf }, resizable: true, enabled_buttons: EnabledButtons { minimize: true, maximize: true, close: true }, decorations: true, transparent: false, focused: true, window_level: Normal, canvas: None, fit_canvas_to_parent: false, prevent_default_event_handling: true, internal: InternalWindowState { minimize_request: None, maximize_request: None, drag_move_request: false, drag_resize_request: None, physical_cursor_position: None }, ime_enabled: false, ime_position: Vec2(0.0, 0.0), window_theme: None, visible: true, skip_taskbar: false, clip_children: true, desired_maximum_frame_latency: None, recognize_pinch_gesture: false, recognize_rotation_gesture: false, recognize_doubletap_gesture: false, recognize_pan_gesture: None, movable_by_window_background: false, fullsize_content_view: false, has_shadow: true, titlebar_shown: true, titlebar_transparent: false, titlebar_show_title: true, titlebar_show_buttons: true, prefers_home_indicator_hidden: false, prefers_status_bar_hidden: false, preferred_screen_edges_deferring_system_gestures: None }, window_icon: WindowIcon { handle: StrongHandle<Image>{ id: Index(AssetIndex { generation: 0, index: 7 }), path: Some(textures/icon.png) } }
   INFO bevy_winit::system: Creating new window MFT Files - Overview (13v0)
  DEBUG bevy_winit::system: Could not set winit window icon during winit window creation: image asset not found - will try again when the asset is ready, entity: 13v0, window: Mut(Window { present_mode: Fifo, mode: Windowed, position: Automatic, resolution: WindowResolution { physical_width: 1280, physical_height: 720, scale_factor_override: None, scale_factor: 1.0 }, title: "MFT Files - Overview", name: None, composite_alpha_mode: Auto, resize_constraints: WindowResizeConstraints { min_width: 180.0, min_height: 120.0, max_width: inf, max_height: inf }, resizable: true, enabled_buttons: EnabledButtons { minimize: true, maximize: true, close: true }, decorations: true, transparent: false, focused: true, window_level: Normal, canvas: None, fit_canvas_to_parent: false, prevent_default_event_handling: true, internal: InternalWindowState { minimize_request: None, maximize_request: None, drag_move_request: false, drag_resize_request: None, physical_cursor_position: None }, ime_enabled: false, ime_position: Vec2(0.0, 0.0), window_theme: None, visible: true, skip_taskbar: false, clip_children: true, desired_maximum_frame_latency: None, recognize_pinch_gesture: false, recognize_rotation_gesture: false, recognize_doubletap_gesture: false, recognize_pan_gesture: None, movable_by_window_background: false, fullsize_content_view: false, has_shadow: true, titlebar_shown: true, titlebar_transparent: false, titlebar_show_title: true, titlebar_show_buttons: true, prefers_home_indicator_hidden: false, prefers_status_bar_hidden: false, preferred_screen_edges_deferring_system_gestures: None }), window_icon: WindowIcon { handle: StrongHandle<Image>{ id: Index(AssetIndex { generation: 0, index: 7 }), path: Some(textures/icon.png) } }
  DEBUG bevy_winit::winit_windows: Display information:  Window physical resolution: 1280x720  Window logical resolution: 1280x720  Monitor name:   Scale factor: 0  Refresh rate (Hz): 0.000
  DEBUG teamy_mft::engine::assets::asset_message_log_plugin: Image asset event, msg: Added { id: AssetId<bevy_image::image::Image>{uuid: 97128bb1-2588-480b-bdc6-87b4adbec477} }
  DEBUG teamy_mft::engine::assets::asset_message_log_plugin: Image asset event, msg: Added { id: AssetId<bevy_image::image::Image>{uuid: d18ad97e-a322-4981-9505-44c59a4b5e46} }
  DEBUG teamy_mft::engine::assets::asset_message_log_plugin: Image asset event, msg: Added { id: AssetId<bevy_image::image::Image>{ index: 0, generation: 0} }
  DEBUG teamy_mft::engine::assets::asset_message_log_plugin: Image asset event, msg: Added { id: AssetId<bevy_image::image::Image>{ index: 1, generation: 0} }
  DEBUG teamy_mft::engine::assets::asset_message_log_plugin: Image asset event, msg: Added { id: AssetId<bevy_image::image::Image>{ index: 2, generation: 0} }
  DEBUG teamy_mft::engine::assets::asset_message_log_plugin: Image asset event, msg: Added { id: AssetId<bevy_image::image::Image>{ index: 3, generation: 0} }
  DEBUG teamy_mft::engine::assets::asset_message_log_plugin: Image asset event, msg: Added { id: AssetId<bevy_image::image::Image>{ index: 6, generation: 0} }
  DEBUG teamy_mft::engine::assets::asset_message_log_plugin: Image asset event, msg: Added { id: AssetId<bevy_image::image::Image>{ index: 5, generation: 0} }
  DEBUG teamy_mft::engine::assets::asset_message_log_plugin: Image asset event, msg: Added { id: AssetId<bevy_image::image::Image>{ index: 4, generation: 0} }
  DEBUG teamy_mft::engine::assets::asset_message_log_plugin: Image asset event, msg: LoadedWithDependencies { id: AssetId<bevy_image::image::Image>{ index: 7, generation: 0} }
  DEBUG bevy_winit::system: Setting window icon, window_entity: 13v0, window.title: "MFT Files - Overview", window_icon.handle: StrongHandle<Image>{ id: Index(AssetIndex { generation: 0, index: 7 }), path: Some(textures/icon.png) }
  DEBUG teamy_mft::engine::assets::asset_message_log_plugin: Image asset event, msg: Removed { id: AssetId<bevy_image::image::Image>{ index: 6, generation: 0} }
  DEBUG teamy_mft::engine::assets::asset_message_log_plugin: Image asset event, msg: Removed { id: AssetId<bevy_image::image::Image>{ index: 3, generation: 0} }
  DEBUG teamy_mft::engine::assets::asset_message_log_plugin: Image asset event, msg: Added { id: AssetId<bevy_image::image::Image>{ index: 7, generation: 0} }

Comment on lines +705 to +721
/// Identify all windows that had their [`WindowIcon`] underlying asset change and mark them for updating the winit icon
#[cfg(feature = "custom_window_icon")]
pub(crate) fn changed_window_icon_asset(
mut commands: bevy_ecs::system::Commands,
windows: Query<(Entity, &WindowIcon)>,
mut asset_messages: MessageReader<AssetEvent<Image>>,
) {
for event in asset_messages.read() {
if let AssetEvent::LoadedWithDependencies { id } | AssetEvent::Modified { id } = event {
for (entity, window_icon) in &windows {
if window_icon.handle.id() == *id {
commands.entity(entity).insert(WindowIconRefreshNeeded);
}
}
}
}
}
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Currently using naïve loop to identify the windows using the handle corresponding to the changed asset.
Could get fancy with observers to maintain a lookup table or something, but for now I went with the low complexity solution.

@TeamDman
Copy link
Author

Here's an attempt at a diagram lol

flowchart TD
    Start([Window with WindowIcon spawned]) --> CheckWindow{Window created<br/>in winit?}
    
    CheckWindow -->|No| CreateWindow[create_windows system<br/>creates winit window]
    CheckWindow -->|Yes| MarkRefresh[Insert WindowIconRefreshNeeded]
    
    CreateWindow --> CheckAssetReady{Image asset<br/>ready?}
    
    CheckAssetReady -->|Yes| SetIconDuringCreation[Set icon during<br/>window creation]
    CheckAssetReady -->|No| LogWillRetry[Log: will try when<br/>asset is ready]
    
    SetIconDuringCreation --> Success1([Icon set ✓])
    LogWillRetry --> WaitForAsset[Wait for asset]
    
    WaitForAsset --> AssetEvent{Asset LoadedWithDependencies<br/>or Modified event}
    AssetEvent --> MarkRefreshAsset[changed_window_icon_asset<br/>inserts WindowIconRefreshNeeded]
    
    MarkRefreshAsset --> SetWinitIcon[set_winit_window_icon system]
    MarkRefresh --> SetWinitIcon
    
    SetWinitIcon --> CheckWindowExists{Winit window<br/>exists?}
    
    CheckWindowExists -->|No| LogWindowNotFound[Log: winit window not found<br/>will try during creation]
    CheckWindowExists -->|Yes| CheckAssetExists{Image asset<br/>exists?}
    
    LogWindowNotFound --> RemoveMarker1[Remove WindowIconRefreshNeeded]
    RemoveMarker1 --> WaitForWindowCreation[Wait for window creation]
    WaitForWindowCreation --> CreateWindow
    
    CheckAssetExists -->|No| LogAssetNotFound[Log: asset not found<br/>will try when ready]
    CheckAssetExists -->|Yes| ConvertIcon{Convert to<br/>winit Icon}
    
    LogAssetNotFound --> RemoveMarker2[Remove WindowIconRefreshNeeded]
    RemoveMarker2 --> WaitForAsset
    
    ConvertIcon -->|Success| ApplyIcon[winit_window.set_window_icon]
    ConvertIcon -->|Failure| LogConversionFailed[Warn: failed to create<br/>winit window icon]
    
    ApplyIcon --> RemoveMarker3[Remove WindowIconRefreshNeeded]
    LogConversionFailed --> RemoveMarker3
    
    RemoveMarker3 --> Success2([Icon set ✓])
    
    %% Handle changes
    WindowIconChanged[WindowIcon component changed] --> ChangedSystem[changed_window_icon system]
    ChangedSystem --> MarkRefresh
    
    AssetChanged[Image asset Modified event] --> AssetChangedSystem[changed_window_icon_asset system]
    AssetChangedSystem --> MarkRefreshAsset
    
    style Success1 fill:#2D5016
    style Success2 fill:#2D5016
    style LogWillRetry fill:#8B6914
    style LogWindowNotFound fill:#8B6914
    style LogAssetNotFound fill:#8B6914
    style LogConversionFailed fill:#8B1538
Loading

TeamDman added a commit to TeamDman/teamy-mft that referenced this pull request Sep 30, 2025
@jf908
Copy link
Contributor

jf908 commented Oct 1, 2025

On Windows, winit provides an API to set window icon to the app icon like so:

use bevy_app::{App, Plugin, Startup};
use bevy_ecs::system::NonSend;
use bevy_winit::WinitWindows;
use winit::platform::windows::IconExtWindows;

/// Only works on Windows
pub struct WindowsIconPlugin;

impl Plugin for WindowsIconPlugin {
    fn build(&self, app: &mut App) {
        app.add_systems(Startup, set_window_icon);
    }
}

fn set_window_icon(windows: NonSend<WinitWindows>) {
    // 32512 is a magic number representing the default application icon
    let icon = IconExtWindows::from_resource(32512, None).ok();

    for window in windows.windows.values() {
        window.set_window_icon(icon.clone());
    }
}

I think this should be considered the default way to set the icon because it avoids relying on the asset system.
Although this doesn't work on X11 or if you wanted a dynamic window icon.

@TeamDman
Copy link
Author

TeamDman commented Oct 1, 2025

As the user I prefer to use Bevy principals like including a component in the bundle when creating a window rather than setting up a separate system for dealing with the timing of winit window availability to set the icon in addition to reimplementing asset loading for when I want to use non-builtin icons in addition to converting formats of the image.

@jf908
Copy link
Contributor

jf908 commented Oct 2, 2025

As the user I prefer to use Bevy principals like including a component in the bundle when creating a window rather than setting up a separate system for dealing with the timing of winit window availability to set the icon in addition to reimplementing asset loading for when I want to use non-builtin icons in addition to converting formats of the image.

Sorry I wasn't that clear but I think that bevy should by default set the window icon to the app icon in addition to providing the functionality you've provided in this PR. They're separate features and should probably be implemented in separate PRs.

I wanted to bring it up because most apps want the window icon to be the same as the app icon and this PR doesn't provide a way to do that easily.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
A-Windowing Platform-agnostic interface layer to run your app in C-Feature A new feature, making something new possible S-Waiting-on-Author The author needs to make changes or address concerns before this can be merged X-Controversial There is active debate or serious implications around merging this PR
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Allow users to set the window's icon
6 participants