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

Feature/issue 1031 window icon #2268

Closed
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
70 changes: 68 additions & 2 deletions crates/bevy_render/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,12 +12,19 @@ pub mod shader;
pub mod texture;
pub mod wireframe;

use std::collections::hash_map::Entry;

use bevy_ecs::{
prelude::{Local, ResMut},
schedule::{ParallelSystemDescriptorCoercion, SystemStage},
system::{IntoExclusiveSystem, IntoSystem, Res},
};
use bevy_transform::TransformSystem;
use bevy_utils::tracing::warn;
use bevy_utils::{
tracing::{error, warn},
HashMap,
};
use bevy_window::{WindowIcon, WindowIconBytes, WindowId, Windows};
use draw::{OutsideFrustum, Visible};

pub use once_cell;
Expand All @@ -40,7 +47,7 @@ pub mod prelude {
use crate::prelude::*;
use base::Msaa;
use bevy_app::prelude::*;
use bevy_asset::{AddAsset, AssetStage};
use bevy_asset::{AddAsset, AssetServer, AssetStage, Assets, Handle, LoadState};
use bevy_ecs::schedule::{StageLabel, SystemLabel};
use camera::{
ActiveCameras, Camera, DepthCalculation, OrthographicProjection, PerspectiveProjection,
Expand Down Expand Up @@ -202,6 +209,7 @@ impl Plugin for RenderPlugin {
.label(RenderSystem::VisibleEntities)
.after(TransformSystem::TransformPropagate),
)
.add_system_to_stage(CoreStage::PostUpdate, window_icon_changed.system())
.add_system_to_stage(
RenderStage::RenderResource,
shader::shader_update_system.system(),
Expand Down Expand Up @@ -248,3 +256,61 @@ fn check_for_render_resource_context(context: Option<Res<Box<dyn RenderResourceC
);
}
}

fn window_icon_changed(
Copy link
Contributor

Choose a reason for hiding this comment

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

Something to consider with this system, is that it gets executed each frame.
I'm curious if we can find some fun way to only have it execute when a window's icon is updated, or when the asset server has loaded it's value.

Probably not, but it's something to think about 😅

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Hmm, yes.
We could do a roundabout way via a command to bevy_winit and an event from there, but I dont think we should involve bevy_winit for that.

mut map: Local<HashMap<WindowId, Handle<Texture>>>,
textures: Res<Assets<Texture>>,
mut windows: ResMut<Windows>,
asset_server: Res<AssetServer>,
) {
for window in windows.iter_mut() {
/* Insert new icon changed */
if let Some(WindowIcon::Path(path)) = window.icon() {
match map.entry(window.id()) {
Entry::Occupied(mut o) => {
if let Some(handle_path) = asset_server.get_handle_path(o.get()) {
if handle_path.path() != path {
o.insert(asset_server.load(path.clone()));
} /* else we are still attempting to load the initial asset */
} /* else the path from the asset is not available yet */
}
Entry::Vacant(v) => {
v.insert(asset_server.load(path.clone()));
}
}
}

/* Poll load state of handle and set the icon */
if let Entry::Occupied(o) = map.entry(window.id()) {
let handle = o.get();
match asset_server.get_load_state(handle) {
LoadState::Loaded => {
let texture = textures.get(handle).unwrap(); /* Safe to unwrap here, because loadstate==loaded is checked */

let window_icon_bytes = WindowIconBytes::from_rgba(
texture.data.clone(),
texture.size.width,
texture.size.height,
);

match window_icon_bytes {
Ok(window_icon_bytes) => {
let window_icon = WindowIcon::from(window_icon_bytes);
window.set_icon(window_icon);
}
Err(e) => error!(
"For handle {:?} the following error was produced: {}",
handle, e
),
}

o.remove();
}
LoadState::Failed => {
o.remove();
}
_ => { /* Do nothing */ }
}
}
}
}
2 changes: 2 additions & 0 deletions crates/bevy_window/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,10 @@ bevy_app = { path = "../bevy_app", version = "0.5.0" }
bevy_ecs = { path = "../bevy_ecs", version = "0.5.0" }
bevy_math = { path = "../bevy_math", version = "0.5.0" }
bevy_utils = { path = "../bevy_utils", version = "0.5.0" }
bevy_asset = { path = "../bevy_asset", version = "0.5.0" }

# other
thiserror = "1.0"

[target.'cfg(target_arch = "wasm32")'.dependencies]
web-sys = "0.3"
124 changes: 123 additions & 1 deletion crates/bevy_window/src/window.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
use bevy_math::{IVec2, Vec2};
use bevy_utils::{tracing::warn, Uuid};
use thiserror::Error;

#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)]
pub struct WindowId(Uuid);
Expand All @@ -18,7 +19,7 @@ impl WindowId {
}
}

use std::fmt;
use std::{fmt, path::PathBuf};

impl fmt::Display for WindowId {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
Expand Down Expand Up @@ -90,6 +91,99 @@ impl WindowResizeConstraints {
}
}

/// Generic icon buffer for a window.
/// Replicates the struct from winit.
///
/// Only allows rgba images.
#[derive(Debug, Clone)]
pub struct WindowIconBytes {
bytes: Vec<u8>,
width: u32,
height: u32,
}

/// Errors that occur while constructing window icons.
#[derive(Error, Debug)]
pub enum WindowIconBytesError {
#[error("32bpp RGBA image buffer expected, but {bytes_length} is not divisible by 4")]
NotRGBA { bytes_length: usize },
#[error("Buffer size {bytes_length} does not match the expected size based on the dimensions {pixel_bytes_length}")]
SizeMismatch {
pixel_bytes_length: usize,
bytes_length: usize,
},
}

/// The icon on a window.
/// The path buffer in the `Path` variant will be passed to the asset server and will be automatically passed to the window backend.
///
/// Make sure that the source image is reasonably sized. Refer to winit's `set_window_icon` function.
#[derive(Debug, Clone)]
pub enum WindowIcon {
Path(PathBuf),
Bytes(WindowIconBytes),
}

impl<T> From<T> for WindowIcon
where
T: Into<PathBuf>,
{
fn from(path: T) -> Self {
WindowIcon::Path(path.into())
}
}

impl From<WindowIconBytes> for WindowIcon {
fn from(window_icon_bytes: WindowIconBytes) -> Self {
WindowIcon::Bytes(window_icon_bytes)
}
}

impl WindowIconBytes {
/// Create a window icon from a rgba image.
///
/// Returns a `WindowIconBytesError` if `bytes` do not add up to a rgba image or the size does not match the specified width and height.
pub fn from_rgba(
bytes: Vec<u8>,
width: u32,
height: u32,
) -> Result<Self, WindowIconBytesError> {
let pixel_count = (width * height) as usize;
let pixel_bytes_length = pixel_count * 4;
let bytes_length = bytes.len();

if bytes_length % 4 != 0 {
Err(WindowIconBytesError::NotRGBA { bytes_length })
} else if pixel_bytes_length != bytes_length {
Err(WindowIconBytesError::SizeMismatch {
pixel_bytes_length,
bytes_length,
})
} else {
Ok(Self {
bytes,
width,
height,
})
}
}

/// Bytes of the rgba icon.
pub fn bytes(&self) -> &[u8] {
&self.bytes
}

/// Width of the icon.
pub fn width(&self) -> u32 {
self.width
}

/// Height of the icon.
pub fn height(&self) -> u32 {
self.height
}
}

/// An operating system window that can present content and receive user input.
///
/// ## Window Sizes
Expand Down Expand Up @@ -125,6 +219,7 @@ pub struct Window {
cursor_position: Option<Vec2>,
focused: bool,
mode: WindowMode,
icon: Option<WindowIcon>,
#[cfg(target_arch = "wasm32")]
pub canvas: Option<String>,
command_queue: Vec<WindowCommand>,
Expand Down Expand Up @@ -176,6 +271,10 @@ pub enum WindowCommand {
SetResizeConstraints {
resize_constraints: WindowResizeConstraints,
},
SetIcon {
window_icon_bytes: WindowIconBytes,
},
ClearIcon,
}

/// Defines the way a window is displayed
Expand Down Expand Up @@ -220,6 +319,7 @@ impl Window {
mode: window_descriptor.mode,
#[cfg(target_arch = "wasm32")]
canvas: window_descriptor.canvas.clone(),
icon: None,
command_queue: Vec::new(),
}
}
Expand Down Expand Up @@ -511,6 +611,26 @@ impl Window {
pub fn is_focused(&self) -> bool {
self.focused
}

#[inline]
pub fn icon(&self) -> Option<&WindowIcon> {
self.icon.as_ref()
}

pub fn set_icon(&mut self, icon: impl Into<WindowIcon> + Clone) {
self.icon = Some(icon.clone().into());

if let WindowIcon::Bytes(window_icon_bytes) = icon.into() {
self.command_queue
.push(WindowCommand::SetIcon { window_icon_bytes });
}
}

pub fn clear_icon(&mut self) {
self.icon = None;

self.command_queue.push(WindowCommand::ClearIcon);
}
}

#[derive(Debug, Clone)]
Expand All @@ -528,6 +648,7 @@ pub struct WindowDescriptor {
pub mode: WindowMode,
#[cfg(target_arch = "wasm32")]
pub canvas: Option<String>,
pub icon_path: Option<PathBuf>,
}

impl Default for WindowDescriptor {
Expand All @@ -546,6 +667,7 @@ impl Default for WindowDescriptor {
mode: WindowMode::Windowed,
#[cfg(target_arch = "wasm32")]
canvas: None,
icon_path: None,
}
}
}
20 changes: 20 additions & 0 deletions crates/bevy_winit/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -19,10 +19,12 @@ use bevy_window::{
WindowBackendScaleFactorChanged, WindowCloseRequested, WindowCreated, WindowFocused,
WindowMoved, WindowResized, WindowScaleFactorChanged, Windows,
};

use winit::{
dpi::PhysicalPosition,
event::{self, DeviceEvent, Event, WindowEvent},
event_loop::{ControlFlow, EventLoop, EventLoopWindowTarget},
window::Icon,
};

use winit::dpi::LogicalSize;
Expand Down Expand Up @@ -158,6 +160,24 @@ fn change_window(world: &mut World) {
window.set_max_inner_size(Some(max_inner_size));
}
}
bevy_window::WindowCommand::SetIcon { window_icon_bytes } => {
let window = winit_windows.get_window(id).unwrap();

/* Winit errors are replicated in the WindowIconBytes constructor, so it is safe to ignore here */
window.set_window_icon(
Icon::from_rgba(
window_icon_bytes.bytes().to_vec(),
window_icon_bytes.width(),
window_icon_bytes.height(),
)
.ok(),
);
}
bevy_window::WindowCommand::ClearIcon => {
let window = winit_windows.get_window(id).unwrap();

window.set_window_icon(None);
}
}
}
}
Expand Down
11 changes: 9 additions & 2 deletions crates/bevy_winit/src/winit_windows.rs
Original file line number Diff line number Diff line change
Expand Up @@ -138,14 +138,21 @@ impl WinitWindows {
let inner_size = winit_window.inner_size();
let scale_factor = winit_window.scale_factor();
self.windows.insert(winit_window.id(), winit_window);
Window::new(

let mut window = Window::new(
window_id,
&window_descriptor,
inner_size.width,
inner_size.height,
scale_factor,
position,
)
);

if let Some(icon_path) = &window_descriptor.icon_path {
window.set_icon(icon_path); /* This will queue up loading the asset and subsequently set the window icon */
}

window
}

pub fn get_window(&self, id: WindowId) -> Option<&winit::window::Window> {
Expand Down
16 changes: 16 additions & 0 deletions examples/window/window_settings.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,13 @@ fn main() {
width: 500.,
height: 300.,
vsync: true,
icon_path: Some("android-res/mipmap-mdpi/ic_launcher.png".into()),
..Default::default()
})
.add_plugins(DefaultPlugins)
.add_system(change_title.system())
.add_system(toggle_cursor.system())
.add_system(toggle_icon.system())
.run();
}

Expand All @@ -33,3 +35,17 @@ fn toggle_cursor(input: Res<Input<KeyCode>>, mut windows: ResMut<Windows>) {
window.set_cursor_visibility(!window.cursor_visible());
}
}

/// This system toggles the windows' icon (on/off) when I is pressed
fn toggle_icon(input: Res<Input<KeyCode>>, mut windows: ResMut<Windows>) {
let window = windows.get_primary_mut().unwrap();
if input.just_pressed(KeyCode::I) {
match window.icon() {
None => {
/* Alternatively you can construct a "buffer-based" WindowIcon and bypass the asset server */
window.set_icon("android-res/mipmap-mdpi/ic_launcher.png");
}
_ => window.clear_icon(),
}
}
}