-
-
Notifications
You must be signed in to change notification settings - Fork 3.2k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Add basic support for window icons #8130
Changes from 2 commits
9f9f374
65ebb16
1ffaebd
f8a208d
67c0a13
5ce861e
f9dcf52
7b6fe97
323e226
08e205f
36c8e48
d9d6384
c9a7cda
8fe20ec
efdf5ce
617c531
cf4aa79
48d0891
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -44,6 +44,7 @@ bevy_render_macros = { path = "macros", version = "0.11.0-dev" } | |
bevy_time = { path = "../bevy_time", version = "0.11.0-dev" } | ||
bevy_transform = { path = "../bevy_transform", version = "0.11.0-dev" } | ||
bevy_window = { path = "../bevy_window", version = "0.11.0-dev" } | ||
bevy_winit = { path = "../bevy_winit", version = "0.11.0-dev" } | ||
bevy_utils = { path = "../bevy_utils", version = "0.11.0-dev" } | ||
bevy_tasks = { path = "../bevy_tasks", version = "0.11.0-dev" } | ||
|
||
|
@@ -78,3 +79,4 @@ encase = { version = "0.5", features = ["glam"] } | |
# For wgpu profiling using tracing. Use `RUST_LOG=info` to also capture the wgpu spans. | ||
profiling = { version = "1", features = ["profile-with-tracing"], optional = true } | ||
async-channel = "1.8" | ||
winit = { version = "0.28", default-features = false } | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Might make sense to re-export There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Agreed, I think that's cleaner. |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,76 @@ | ||
use crate::texture::Image; | ||
use bevy_asset::{Assets, Handle}; | ||
use bevy_ecs::{ | ||
prelude::{Component, Entity, NonSendMut, Query, Res}, | ||
system::Commands, | ||
}; | ||
use bevy_log::{error, info}; | ||
use bevy_winit::WinitWindows; | ||
use winit::window::Icon; | ||
|
||
/// An icon that can be placed at the top left of the window. | ||
#[derive(Component, Debug)] | ||
pub struct WindowIcon(pub Option<Handle<Image>>); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. A simple |
||
|
||
/// Set or unset the window icon, depending on whether `Some(image_handle)` or `None` is provided. | ||
/// | ||
/// # Example | ||
/// ```rust,no_run | ||
/// use bevy_app::{App, Startup, Update}; | ||
/// use bevy_asset::AssetServer; | ||
/// use bevy_ecs::prelude::*; | ||
/// use bevy_render::texture::{set_window_icon, WindowIcon}; | ||
/// | ||
/// fn setup(mut commands: Commands, asset_server: Res<AssetServer>) { | ||
/// let icon_handle = asset_server.load("branding/icon.png"); | ||
/// commands.spawn(WindowIcon(Some(icon_handle))); | ||
/// } | ||
/// | ||
/// fn main() { | ||
/// App::new() | ||
/// .add_systems(Startup, setup) | ||
/// .add_systems(Update, set_window_icon) | ||
/// .run(); | ||
/// } | ||
/// ``` | ||
/// | ||
/// This functionality [is only known to work on Windows and X11](https://docs.rs/winit/latest/winit/window/struct.Window.html#method.set_window_icon). | ||
pub fn set_window_icon( | ||
images: Res<Assets<Image>>, | ||
mut commands: Commands, | ||
mut query: Query<(Entity, &mut WindowIcon)>, | ||
mut winit_windows: NonSendMut<WinitWindows>, | ||
) { | ||
for (entity, window_icon) in query.iter_mut() { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This whole system could probably be simplified significantly. Basically: for (id, window) in &mut winit_windows.windows {
if let Ok(WindowIcon(maybe_handle)) = query.get(id) {
window.set_window_icon(maybe_handle);
} else {
window.set_window_icon(None);
}
} There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I tried to stick pretty close to this. One thing I noticed is that it might be more efficient for the outer loop to start with the query and check for associated windows than the other way around. Probably not needed, though. |
||
let icon = { | ||
if let Some(image) = &window_icon.0 { | ||
let Some(icon) = images.get(image) else { continue }; | ||
let result: Result<Icon, _> = icon.clone().try_into(); | ||
|
||
match result { | ||
Ok(icon) => Some(icon), | ||
Err(err) => { | ||
error!("failed to set window icon: {}", err); | ||
commands.entity(entity).remove::<WindowIcon>(); | ||
continue; | ||
} | ||
} | ||
} else { | ||
None | ||
} | ||
}; | ||
|
||
if let Some(icon) = &icon { | ||
for (_id, window) in &mut winit_windows.windows { | ||
window.set_window_icon(Some(icon.clone())); | ||
} | ||
} else { | ||
for (_id, window) in &mut winit_windows.windows { | ||
window.set_window_icon(None); | ||
} | ||
} | ||
|
||
info!("window icon set"); | ||
commands.entity(entity).remove::<WindowIcon>(); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Definitely don't think we should be removing the Window icon component here once it's set. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I was worried about a busy loop of always attempting to check whether an icon had been set and then exiting early if it was. I looked into using the typestate pattern, e.g., Is there a way to remove this code from the hot path once the icon has been set? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Set a run condition on the system itself, checking if any of the There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. As I acquaint myself with Bevy, I'm realizing that the If the state machine is stored on the component itself, mutating the component to update the state enum will interfere with the I'm sure this is all just me misunderstanding how one is supposed to do things in Bevy when an asset needs to be fully loaded. I'll poke around and try to better understand the desired approach in this case. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'm thinking something similar to the implementation for run_once might work here. |
||
} | ||
} | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. There's two uncorrelated collections here — the There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think we probably want to align the window icon to the corresponding window, since we're storing it as a component. |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -11,11 +11,13 @@ use crate::{ | |
renderer::{RenderDevice, RenderQueue}, | ||
texture::BevyDefault, | ||
}; | ||
use anyhow::anyhow; | ||
use bevy_asset::HandleUntyped; | ||
use bevy_derive::{Deref, DerefMut}; | ||
use bevy_ecs::system::{lifetimeless::SRes, Resource, SystemParamItem}; | ||
use bevy_math::Vec2; | ||
use bevy_reflect::{FromReflect, Reflect, TypeUuid}; | ||
use winit::window::Icon; | ||
|
||
use std::hash::Hash; | ||
use thiserror::Error; | ||
|
@@ -613,6 +615,23 @@ impl CompressedImageFormats { | |
} | ||
} | ||
|
||
// Convert an [`Image`] to `winit::window::Icon`. | ||
impl TryInto<Icon> for Image { | ||
type Error = anyhow::Error; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. A previous PR had a purpose-specific error type, which might be good to use here (not sure). There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yeah I'd prefer to use a custom error type over anyhow. |
||
|
||
fn try_into(self) -> Result<Icon, Self::Error> { | ||
let Ok(icon) = self.try_into_dynamic() else { | ||
return Err(anyhow!("failed to convert Image to DynamicImage")); | ||
}; | ||
|
||
let width = icon.width(); | ||
let height = icon.height(); | ||
let data = icon.into_rgba8().into_raw(); | ||
Icon::from_rgba(data, width, height) | ||
.map_err(|err| anyhow!("failed to convert image to winit::window::Icon: {}", err)) | ||
} | ||
} | ||
|
||
#[cfg(test)] | ||
mod test { | ||
|
||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -4,6 +4,7 @@ | |
use bevy::{ | ||
diagnostic::{FrameTimeDiagnosticsPlugin, LogDiagnosticsPlugin}, | ||
prelude::*, | ||
render::texture::{set_window_icon, WindowIcon}, | ||
window::{CursorGrabMode, PresentMode, WindowLevel}, | ||
}; | ||
|
||
|
@@ -24,9 +25,11 @@ fn main() { | |
})) | ||
.add_plugin(LogDiagnosticsPlugin::default()) | ||
.add_plugin(FrameTimeDiagnosticsPlugin) | ||
.add_systems(Startup, setup) | ||
.add_systems( | ||
Update, | ||
( | ||
set_window_icon, | ||
change_title, | ||
toggle_cursor, | ||
toggle_vsync, | ||
|
@@ -37,6 +40,12 @@ fn main() { | |
.run(); | ||
} | ||
|
||
/// Add an icon to the window task bar. Only works in Windows and Linux. | ||
fn setup(mut commands: Commands, asset_server: Res<AssetServer>) { | ||
let icon_handle = asset_server.load("branding/icon.png"); | ||
commands.spawn(WindowIcon(Some(icon_handle))); | ||
} | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It might be possible to simplify all of this by adding an A benefit of having the developer queue up the two systems is that they could potentially change the icon during the running of the game. I'm not sure anyone would ever want that, but this is something that was demonstrated in an earlier PR for this story. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yeah, let's keep this simple here. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
By "simple," are you thinking API-simple (adding a field to Thinking you were probably referring to the enum idea, so sticking with the current approach. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yeah, I meant that I prefer the current approach :) There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Once it occurred to me that several moving parts would need to be orchestrated in order to make this work without issues, I took the liberty of making a plugin. I'm not attached to it, though, so no problem if it's dropped. |
||
|
||
/// 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<Input<KeyCode>>, mut windows: Query<&mut Window>) { | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This should be made into an optional (but on by default) dependency, behind a
winit
feature flag.