Skip to content
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

Draft
wants to merge 18 commits into
base: main
Choose a base branch
from

Conversation

emwalker
Copy link

@emwalker emwalker commented Mar 19, 2023

Objective

Fixes #1031.

This PR adds an icon to the left of the title in the window taskbar on Windows and X11.

Looking for high-level feedback on whether this implementation is going in the right direction before cleaning up and submitting. A lot of the concepts in Bevy are new to me, and it sounds like the approach that has been suggested is a workaround and might not have a lot of prior art to consult. More context here.

2023-03-19_08-31

Solution

  • TBD

Open questions

  • What to do about failing test run in the bevy_render crate? Perhaps this will require adding back bevy_winit = ["bevy_winit/x11", "bevy_winit/wayland"] and adding the wayland-sys library to CI. But there might be another way to fix the failing test run.
  • Is there a modification to the current approach that will allow the systems to run several times and update the icon each time?

Changelog

Add an icon to the left of the title in the window taskbar on Windows and X11.

Migration Guide

N/A

fn setup(mut commands: Commands, asset_server: Res<AssetServer>) {
let icon_handle = asset_server.load("branding/icon.png");
commands.spawn(WindowIcon(Some(icon_handle)));
}
Copy link
Author

@emwalker emwalker Mar 19, 2023

Choose a reason for hiding this comment

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

It might be possible to simplify all of this by adding an icon_path: Option<PathBuf> or an enum with variants for a path and for bytes for specific image formats to a field on the Window struct and then adding the two systems above when one of the low-level Bevy plugins runs. But first I thought it would be good to check whether this PR gets the general idea right.

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.

Copy link
Member

Choose a reason for hiding this comment

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

Yeah, let's keep this simple here.

Copy link
Author

@emwalker emwalker Mar 19, 2023

Choose a reason for hiding this comment

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

Yeah, let's keep this simple here.

By "simple," are you thinking API-simple (adding a field to Window), or implementation-simple (sticking with the current approach)?

Thinking you were probably referring to the enum idea, so sticking with the current approach.

Copy link
Member

Choose a reason for hiding this comment

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

Yeah, I meant that I prefer the current approach :)

Copy link
Author

Choose a reason for hiding this comment

The 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.

info!("window icon set");
commands.entity(entity).remove::<WindowIcon>();
}
}
Copy link
Author

@emwalker emwalker Mar 19, 2023

Choose a reason for hiding this comment

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

There's two uncorrelated collections here — the WindowIcons in flight, and the various windows. In this PR we're just ignoring that complexity. I wasn't sure how much precision would be needed in a first pass.

Copy link
Member

Choose a reason for hiding this comment

The 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.

@@ -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 }
Copy link
Author

Choose a reason for hiding this comment

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

Might make sense to re-export winit::window::Icon from bevy_winit instead of including winit here.

Copy link
Member

Choose a reason for hiding this comment

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

Agreed, I think that's cleaner.

@@ -613,6 +615,23 @@ impl CompressedImageFormats {
}
}

// Convert an [`Image`] to `winit::window::Icon`.
impl TryInto<Icon> for Image {
type Error = anyhow::Error;
Copy link
Author

@emwalker emwalker Mar 19, 2023

Choose a reason for hiding this comment

The 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).

Copy link
Member

Choose a reason for hiding this comment

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

Yeah I'd prefer to use a custom error type over anyhow.

@alice-i-cecile alice-i-cecile added C-Enhancement A new feature A-Windowing Platform-agnostic interface layer to run your app in labels Mar 19, 2023
}

info!("window icon set");
commands.entity(entity).remove::<WindowIcon>();
Copy link
Member

Choose a reason for hiding this comment

The 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.

Copy link
Author

@emwalker emwalker Mar 19, 2023

Choose a reason for hiding this comment

The 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., WindowIcon<Loading>, WindowIcon<Loaded>, etc., and then only querying for WindowIcon<Loading>, but ran into difficulties with that.

Is there a way to remove this code from the hot path once the icon has been set?

Copy link
Member

Choose a reason for hiding this comment

The 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 WindowIcon components have been changed. That will stop it from running at all, and will be extremely quick to check.

Copy link
Author

Choose a reason for hiding this comment

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

As I acquaint myself with Bevy, I'm realizing that the Changed<WindowIcon> state will only last a tick, until the next &mut WindowIcon occurs, while the image asset itself will take several ticks to load, by which point the Changed<WindowIcon> state will have been cleared two or more ticks in the past. This delay between the Changed<T> and the loaded image asset seems to rule out a simple read-only check of some kind, instead requiring a component-specific state machine to track when things are in place to set the icon.

If the state machine is stored on the component itself, mutating the component to update the state enum will interfere with the Changed<WindowIcon> check. And polling in the hot path for whether to set the icon seems to be unavoidable unless loading is carried out in a separate app state. Using app state seems to have the drawback of not being generic and instead requiring an app-specific setup.

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.

Copy link
Author

Choose a reason for hiding this comment

The 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.

mut query: Query<(Entity, &mut WindowIcon)>,
mut winit_windows: NonSendMut<WinitWindows>,
) {
for (entity, window_icon) in query.iter_mut() {
Copy link
Member

Choose a reason for hiding this comment

The 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);
  } 
}

Copy link
Author

Choose a reason for hiding this comment

The 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.


/// An icon that can be placed at the top left of the window.
#[derive(Component, Debug)]
pub struct WindowIcon(pub Option<Handle<Image>>);
Copy link
Member

Choose a reason for hiding this comment

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

A simple new(handle: Handle<Image>) method would go a long way to improving usability here :)

@@ -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" }
Copy link
Member

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.

@alice-i-cecile
Copy link
Member

Okay good! The overall module structure like this is much less bad than I was worried about. I think we can make the underlying system a bit less janky, but once that's done this should work nicely.

@JMS55
Copy link
Contributor

JMS55 commented Mar 19, 2023

Does it make sense to provide an API for setting the window icon? On Linux, you're supposed to use a .desktop file (which also lets it show up in UI's). On Windows, you're supposed to embed a resource file into the exe. On macOS I'm not sure.

@alice-i-cecile
Copy link
Member

alice-i-cecile commented Mar 19, 2023

Does it make sense to provide an API for setting the window icon? On Linux, you're supposed to use a .desktop file (which also lets it show up in UI's). On Windows, you're supposed to embed a resource file into the exe. On macOS I'm not sure.

I like the cross-platform nature of this: it'll make the build process significantly less annoying when releasing games. Although maybe this approach messes up how apps show up in e.g. the launcher too?

@mockersf
Copy link
Member

mockersf commented Mar 21, 2023

Does it make sense to provide an API for setting the window icon? On Linux, you're supposed to use a .desktop file (which also lets it show up in UI's). On Windows, you're supposed to embed a resource file into the exe. On macOS I'm not sure.

macOS, Android and iOS are also handled by providing a file with the binary (and packaged in the apk or app)

@mockersf
Copy link
Member

mockersf commented Mar 21, 2023

making bevy_render depends (optionally but enabled by default) on bevy_winit doesn't feel good for something that won't work on most platforms

@emwalker
Copy link
Author

emwalker commented Mar 21, 2023

making bevy_render depends (optionally but enabled by default) on bevy_winit doesn't feel good for something that won't work on most platforms

I think everyone feels weird about it. See this thread for context.

@emwalker
Copy link
Author

Looking into why the build is failing for wayland-sys. Someone might what's going on here.

@mockersf
Copy link
Member

mockersf commented Mar 21, 2023

Looking into why the build is failing for wayland-sys. Someone might what's going on here.

you enabled the wayland feature of bevy_winit but wayland dependencies are not installed in CI. You change makes it always enabled.

@emwalker
Copy link
Author

emwalker commented Mar 21, 2023

you enabled the wayland feature of bevy_winit but wayland dependencies are not installed in CI. You change makes it always enabled.

I probably misunderstood this request, in that case. Or, if not, I'm thinking I should add the missing dependency to CI.

@@ -27,6 +28,8 @@ wgpu_trace = ["wgpu/trace"]
ci_limits = []
webgl = ["wgpu/webgl"]

bevy_winit = ["bevy_winit/x11", "bevy_winit/wayland"]
Copy link
Member

Choose a reason for hiding this comment

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

CI failures comes from this line, none of those features should be enabled here

Copy link
Author

Choose a reason for hiding this comment

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

Thank you! That fixed things.

Copy link
Author

Choose a reason for hiding this comment

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

Now I remember why those were added:

2023-03-22_16-39

@emwalker
Copy link
Author

emwalker commented Mar 25, 2023

Is this approach in this PR basically sound? Should I clean it up and move it out of draft? Or should I abandon it and let someone else take a crack at it? I'm find abandoning it, if people think this isn't quite the right approach and it's going to be hard to get it into shape.

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-Enhancement A new feature
Projects
Status: Candidate
Development

Successfully merging this pull request may close these issues.

A field in WindowDescriptor resource to set the window's icon ?
5 participants