diff --git a/crates/bevy_render/src/lib.rs b/crates/bevy_render/src/lib.rs index 0bd7c77f586ee..daff0d6444855 100644 --- a/crates/bevy_render/src/lib.rs +++ b/crates/bevy_render/src/lib.rs @@ -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; @@ -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, @@ -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(), @@ -248,3 +256,61 @@ fn check_for_render_resource_context(context: Option>>, + textures: Res>, + mut windows: ResMut, + asset_server: Res, +) { + 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 */ } + } + } + } +} diff --git a/crates/bevy_window/Cargo.toml b/crates/bevy_window/Cargo.toml index b7064f986612e..0d0b453bb22d1 100644 --- a/crates/bevy_window/Cargo.toml +++ b/crates/bevy_window/Cargo.toml @@ -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" diff --git a/crates/bevy_window/src/window.rs b/crates/bevy_window/src/window.rs index be3a6c0f99230..89e5f1897fda1 100644 --- a/crates/bevy_window/src/window.rs +++ b/crates/bevy_window/src/window.rs @@ -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); @@ -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 { @@ -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, + 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 From for WindowIcon +where + T: Into, +{ + fn from(path: T) -> Self { + WindowIcon::Path(path.into()) + } +} + +impl From 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, + width: u32, + height: u32, + ) -> Result { + 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 @@ -125,6 +219,7 @@ pub struct Window { cursor_position: Option, focused: bool, mode: WindowMode, + icon: Option, #[cfg(target_arch = "wasm32")] pub canvas: Option, command_queue: Vec, @@ -176,6 +271,10 @@ pub enum WindowCommand { SetResizeConstraints { resize_constraints: WindowResizeConstraints, }, + SetIcon { + window_icon_bytes: WindowIconBytes, + }, + ClearIcon, } /// Defines the way a window is displayed @@ -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(), } } @@ -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 + 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)] @@ -528,6 +648,7 @@ pub struct WindowDescriptor { pub mode: WindowMode, #[cfg(target_arch = "wasm32")] pub canvas: Option, + pub icon_path: Option, } impl Default for WindowDescriptor { @@ -546,6 +667,7 @@ impl Default for WindowDescriptor { mode: WindowMode::Windowed, #[cfg(target_arch = "wasm32")] canvas: None, + icon_path: None, } } } diff --git a/crates/bevy_winit/src/lib.rs b/crates/bevy_winit/src/lib.rs index 3ba197c05c9f2..6021546d67084 100644 --- a/crates/bevy_winit/src/lib.rs +++ b/crates/bevy_winit/src/lib.rs @@ -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; @@ -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); + } } } } diff --git a/crates/bevy_winit/src/winit_windows.rs b/crates/bevy_winit/src/winit_windows.rs index e3a3c518c0421..d507e4ad4890f 100644 --- a/crates/bevy_winit/src/winit_windows.rs +++ b/crates/bevy_winit/src/winit_windows.rs @@ -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> { diff --git a/examples/window/window_settings.rs b/examples/window/window_settings.rs index 3c7719916ac2e..5a5ab4e5c9196 100644 --- a/examples/window/window_settings.rs +++ b/examples/window/window_settings.rs @@ -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(); } @@ -33,3 +35,17 @@ fn toggle_cursor(input: Res>, mut windows: ResMut) { window.set_cursor_visibility(!window.cursor_visible()); } } + +/// This system toggles the windows' icon (on/off) when I is pressed +fn toggle_icon(input: Res>, mut windows: ResMut) { + 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(), + } + } +}