diff --git a/Cargo.toml b/Cargo.toml index 9c43da5a15bd17..807a06f62eca82 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -2340,6 +2340,17 @@ description = "Demonstrates how to create a node with a border" category = "UI (User Interface)" wasm = true +[[example]] +name = "rounded_borders" +path = "examples/ui/rounded_borders.rs" +doc-scrape-examples = true + +[package.metadata.example.rounded_borders] +name = "Rounded Borders" +description = "Demonstrates how to create a node with a rounded border" +category = "UI (User Interface)" +wasm = true + [[example]] name = "button" path = "examples/ui/button.rs" diff --git a/crates/bevy_animation/src/graph.rs b/crates/bevy_animation/src/graph.rs index aba53f1d7adfae..01a270ed5385d8 100644 --- a/crates/bevy_animation/src/graph.rs +++ b/crates/bevy_animation/src/graph.rs @@ -6,7 +6,6 @@ use std::ops::{Index, IndexMut}; use bevy_asset::io::Reader; use bevy_asset::{Asset, AssetId, AssetLoader, AssetPath, AsyncReadExt as _, Handle, LoadContext}; use bevy_reflect::{Reflect, ReflectSerialize}; -use bevy_utils::BoxedFuture; use petgraph::graph::{DiGraph, NodeIndex}; use ron::de::SpannedError; use serde::{Deserialize, Serialize}; @@ -336,40 +335,37 @@ impl AssetLoader for AnimationGraphAssetLoader { type Error = AnimationGraphLoadError; - fn load<'a>( + async fn load<'a>( &'a self, - reader: &'a mut Reader, + reader: &'a mut Reader<'_>, _: &'a Self::Settings, - load_context: &'a mut LoadContext, - ) -> BoxedFuture<'a, Result> { - Box::pin(async move { - let mut bytes = Vec::new(); - reader.read_to_end(&mut bytes).await?; - - // Deserialize a `SerializedAnimationGraph` directly, so that we can - // get the list of the animation clips it refers to and load them. - let mut deserializer = ron::de::Deserializer::from_bytes(&bytes)?; - let serialized_animation_graph = - SerializedAnimationGraph::deserialize(&mut deserializer) - .map_err(|err| deserializer.span_error(err))?; - - // Load all `AssetPath`s to convert from a - // `SerializedAnimationGraph` to a real `AnimationGraph`. - Ok(AnimationGraph { - graph: serialized_animation_graph.graph.map( - |_, serialized_node| AnimationGraphNode { - clip: serialized_node.clip.as_ref().map(|clip| match clip { - SerializedAnimationClip::AssetId(asset_id) => Handle::Weak(*asset_id), - SerializedAnimationClip::AssetPath(asset_path) => { - load_context.load(asset_path) - } - }), - weight: serialized_node.weight, - }, - |_, _| (), - ), - root: serialized_animation_graph.root, - }) + load_context: &'a mut LoadContext<'_>, + ) -> Result { + let mut bytes = Vec::new(); + reader.read_to_end(&mut bytes).await?; + + // Deserialize a `SerializedAnimationGraph` directly, so that we can + // get the list of the animation clips it refers to and load them. + let mut deserializer = ron::de::Deserializer::from_bytes(&bytes)?; + let serialized_animation_graph = SerializedAnimationGraph::deserialize(&mut deserializer) + .map_err(|err| deserializer.span_error(err))?; + + // Load all `AssetPath`s to convert from a + // `SerializedAnimationGraph` to a real `AnimationGraph`. + Ok(AnimationGraph { + graph: serialized_animation_graph.graph.map( + |_, serialized_node| AnimationGraphNode { + clip: serialized_node.clip.as_ref().map(|clip| match clip { + SerializedAnimationClip::AssetId(asset_id) => Handle::Weak(*asset_id), + SerializedAnimationClip::AssetPath(asset_path) => { + load_context.load(asset_path) + } + }), + weight: serialized_node.weight, + }, + |_, _| (), + ), + root: serialized_animation_graph.root, }) } diff --git a/crates/bevy_asset/src/io/android.rs b/crates/bevy_asset/src/io/android.rs index 38791b35e0071c..18daac59496453 100644 --- a/crates/bevy_asset/src/io/android.rs +++ b/crates/bevy_asset/src/io/android.rs @@ -2,7 +2,6 @@ use crate::io::{ get_meta_path, AssetReader, AssetReaderError, EmptyPathStream, PathStream, Reader, VecReader, }; use bevy_utils::tracing::error; -use bevy_utils::BoxedFuture; use std::{ffi::CString, path::Path}; /// [`AssetReader`] implementation for Android devices, built on top of Android's [`AssetManager`]. @@ -17,57 +16,47 @@ use std::{ffi::CString, path::Path}; pub struct AndroidAssetReader; impl AssetReader for AndroidAssetReader { - fn read<'a>( - &'a self, - path: &'a Path, - ) -> BoxedFuture<'a, Result>, AssetReaderError>> { - Box::pin(async move { - let asset_manager = bevy_winit::ANDROID_APP - .get() - .expect("Bevy must be setup with the #[bevy_main] macro on Android") - .asset_manager(); - let mut opened_asset = asset_manager - .open(&CString::new(path.to_str().unwrap()).unwrap()) - .ok_or(AssetReaderError::NotFound(path.to_path_buf()))?; - let bytes = opened_asset.buffer()?; - let reader: Box = Box::new(VecReader::new(bytes.to_vec())); - Ok(reader) - }) + async fn read<'a>(&'a self, path: &'a Path) -> Result>, AssetReaderError> { + let asset_manager = bevy_winit::ANDROID_APP + .get() + .expect("Bevy must be setup with the #[bevy_main] macro on Android") + .asset_manager(); + let mut opened_asset = asset_manager + .open(&CString::new(path.to_str().unwrap()).unwrap()) + .ok_or(AssetReaderError::NotFound(path.to_path_buf()))?; + let bytes = opened_asset.buffer()?; + let reader: Box = Box::new(VecReader::new(bytes.to_vec())); + Ok(reader) } - fn read_meta<'a>( - &'a self, - path: &'a Path, - ) -> BoxedFuture<'a, Result>, AssetReaderError>> { - Box::pin(async move { - let meta_path = get_meta_path(path); - let asset_manager = bevy_winit::ANDROID_APP - .get() - .expect("Bevy must be setup with the #[bevy_main] macro on Android") - .asset_manager(); - let mut opened_asset = asset_manager - .open(&CString::new(meta_path.to_str().unwrap()).unwrap()) - .ok_or(AssetReaderError::NotFound(meta_path))?; - let bytes = opened_asset.buffer()?; - let reader: Box = Box::new(VecReader::new(bytes.to_vec())); - Ok(reader) - }) + async fn read_meta<'a>(&'a self, path: &'a Path) -> Result>, AssetReaderError> { + let meta_path = get_meta_path(path); + let asset_manager = bevy_winit::ANDROID_APP + .get() + .expect("Bevy must be setup with the #[bevy_main] macro on Android") + .asset_manager(); + let mut opened_asset = asset_manager + .open(&CString::new(meta_path.to_str().unwrap()).unwrap()) + .ok_or(AssetReaderError::NotFound(meta_path))?; + let bytes = opened_asset.buffer()?; + let reader: Box = Box::new(VecReader::new(bytes.to_vec())); + Ok(reader) } - fn read_directory<'a>( + async fn read_directory<'a>( &'a self, _path: &'a Path, - ) -> BoxedFuture<'a, Result, AssetReaderError>> { + ) -> Result, AssetReaderError> { let stream: Box = Box::new(EmptyPathStream); error!("Reading directories is not supported with the AndroidAssetReader"); - Box::pin(async move { Ok(stream) }) + Ok(stream) } - fn is_directory<'a>( + async fn is_directory<'a>( &'a self, _path: &'a Path, - ) -> BoxedFuture<'a, std::result::Result> { + ) -> std::result::Result { error!("Reading directories is not supported with the AndroidAssetReader"); - Box::pin(async move { Ok(false) }) + Ok(false) } } diff --git a/crates/bevy_asset/src/io/file/file_asset.rs b/crates/bevy_asset/src/io/file/file_asset.rs index aa209131401111..5826fe097ddb33 100644 --- a/crates/bevy_asset/src/io/file/file_asset.rs +++ b/crates/bevy_asset/src/io/file/file_asset.rs @@ -3,7 +3,6 @@ use crate::io::{ Reader, Writer, }; use async_fs::{read_dir, File}; -use bevy_utils::BoxedFuture; use futures_lite::StreamExt; use std::path::Path; @@ -11,215 +10,168 @@ use std::path::Path; use super::{FileAssetReader, FileAssetWriter}; impl AssetReader for FileAssetReader { - fn read<'a>( - &'a self, - path: &'a Path, - ) -> BoxedFuture<'a, Result>, AssetReaderError>> { - Box::pin(async move { - let full_path = self.root_path.join(path); - match File::open(&full_path).await { - Ok(file) => { - let reader: Box = Box::new(file); - Ok(reader) - } - Err(e) => { - if e.kind() == std::io::ErrorKind::NotFound { - Err(AssetReaderError::NotFound(full_path)) - } else { - Err(e.into()) - } + async fn read<'a>(&'a self, path: &'a Path) -> Result>, AssetReaderError> { + let full_path = self.root_path.join(path); + match File::open(&full_path).await { + Ok(file) => { + let reader: Box = Box::new(file); + Ok(reader) + } + Err(e) => { + if e.kind() == std::io::ErrorKind::NotFound { + Err(AssetReaderError::NotFound(full_path)) + } else { + Err(e.into()) } } - }) + } } - fn read_meta<'a>( - &'a self, - path: &'a Path, - ) -> BoxedFuture<'a, Result>, AssetReaderError>> { + async fn read_meta<'a>(&'a self, path: &'a Path) -> Result>, AssetReaderError> { let meta_path = get_meta_path(path); - Box::pin(async move { - let full_path = self.root_path.join(meta_path); - match File::open(&full_path).await { - Ok(file) => { - let reader: Box = Box::new(file); - Ok(reader) - } - Err(e) => { - if e.kind() == std::io::ErrorKind::NotFound { - Err(AssetReaderError::NotFound(full_path)) - } else { - Err(e.into()) - } + let full_path = self.root_path.join(meta_path); + match File::open(&full_path).await { + Ok(file) => { + let reader: Box = Box::new(file); + Ok(reader) + } + Err(e) => { + if e.kind() == std::io::ErrorKind::NotFound { + Err(AssetReaderError::NotFound(full_path)) + } else { + Err(e.into()) } } - }) + } } - fn read_directory<'a>( + async fn read_directory<'a>( &'a self, path: &'a Path, - ) -> BoxedFuture<'a, Result, AssetReaderError>> { - Box::pin(async move { - let full_path = self.root_path.join(path); - match read_dir(&full_path).await { - Ok(read_dir) => { - let root_path = self.root_path.clone(); - let mapped_stream = read_dir.filter_map(move |f| { - f.ok().and_then(|dir_entry| { - let path = dir_entry.path(); - // filter out meta files as they are not considered assets - if let Some(ext) = path.extension().and_then(|e| e.to_str()) { - if ext.eq_ignore_ascii_case("meta") { - return None; - } + ) -> Result, AssetReaderError> { + let full_path = self.root_path.join(path); + match read_dir(&full_path).await { + Ok(read_dir) => { + let root_path = self.root_path.clone(); + let mapped_stream = read_dir.filter_map(move |f| { + f.ok().and_then(|dir_entry| { + let path = dir_entry.path(); + // filter out meta files as they are not considered assets + if let Some(ext) = path.extension().and_then(|e| e.to_str()) { + if ext.eq_ignore_ascii_case("meta") { + return None; } - let relative_path = path.strip_prefix(&root_path).unwrap(); - Some(relative_path.to_owned()) - }) - }); - let read_dir: Box = Box::new(mapped_stream); - Ok(read_dir) - } - Err(e) => { - if e.kind() == std::io::ErrorKind::NotFound { - Err(AssetReaderError::NotFound(full_path)) - } else { - Err(e.into()) - } + } + let relative_path = path.strip_prefix(&root_path).unwrap(); + Some(relative_path.to_owned()) + }) + }); + let read_dir: Box = Box::new(mapped_stream); + Ok(read_dir) + } + Err(e) => { + if e.kind() == std::io::ErrorKind::NotFound { + Err(AssetReaderError::NotFound(full_path)) + } else { + Err(e.into()) } } - }) + } } - fn is_directory<'a>( - &'a self, - path: &'a Path, - ) -> BoxedFuture<'a, Result> { - Box::pin(async move { - let full_path = self.root_path.join(path); - let metadata = full_path - .metadata() - .map_err(|_e| AssetReaderError::NotFound(path.to_owned()))?; - Ok(metadata.file_type().is_dir()) - }) + async fn is_directory<'a>(&'a self, path: &'a Path) -> Result { + let full_path = self.root_path.join(path); + let metadata = full_path + .metadata() + .map_err(|_e| AssetReaderError::NotFound(path.to_owned()))?; + Ok(metadata.file_type().is_dir()) } } impl AssetWriter for FileAssetWriter { - fn write<'a>( - &'a self, - path: &'a Path, - ) -> BoxedFuture<'a, Result, AssetWriterError>> { - Box::pin(async move { - let full_path = self.root_path.join(path); - if let Some(parent) = full_path.parent() { - async_fs::create_dir_all(parent).await?; - } - let file = File::create(&full_path).await?; - let writer: Box = Box::new(file); - Ok(writer) - }) + async fn write<'a>(&'a self, path: &'a Path) -> Result, AssetWriterError> { + let full_path = self.root_path.join(path); + if let Some(parent) = full_path.parent() { + async_fs::create_dir_all(parent).await?; + } + let file = File::create(&full_path).await?; + let writer: Box = Box::new(file); + Ok(writer) } - fn write_meta<'a>( - &'a self, - path: &'a Path, - ) -> BoxedFuture<'a, Result, AssetWriterError>> { - Box::pin(async move { - let meta_path = get_meta_path(path); - let full_path = self.root_path.join(meta_path); - if let Some(parent) = full_path.parent() { - async_fs::create_dir_all(parent).await?; - } - let file = File::create(&full_path).await?; - let writer: Box = Box::new(file); - Ok(writer) - }) + async fn write_meta<'a>(&'a self, path: &'a Path) -> Result, AssetWriterError> { + let meta_path = get_meta_path(path); + let full_path = self.root_path.join(meta_path); + if let Some(parent) = full_path.parent() { + async_fs::create_dir_all(parent).await?; + } + let file = File::create(&full_path).await?; + let writer: Box = Box::new(file); + Ok(writer) } - fn remove<'a>(&'a self, path: &'a Path) -> BoxedFuture<'a, Result<(), AssetWriterError>> { - Box::pin(async move { - let full_path = self.root_path.join(path); - async_fs::remove_file(full_path).await?; - Ok(()) - }) + async fn remove<'a>(&'a self, path: &'a Path) -> Result<(), AssetWriterError> { + let full_path = self.root_path.join(path); + async_fs::remove_file(full_path).await?; + Ok(()) } - fn remove_meta<'a>(&'a self, path: &'a Path) -> BoxedFuture<'a, Result<(), AssetWriterError>> { - Box::pin(async move { - let meta_path = get_meta_path(path); - let full_path = self.root_path.join(meta_path); - async_fs::remove_file(full_path).await?; - Ok(()) - }) + async fn remove_meta<'a>(&'a self, path: &'a Path) -> Result<(), AssetWriterError> { + let meta_path = get_meta_path(path); + let full_path = self.root_path.join(meta_path); + async_fs::remove_file(full_path).await?; + Ok(()) } - fn rename<'a>( + async fn rename<'a>( &'a self, old_path: &'a Path, new_path: &'a Path, - ) -> BoxedFuture<'a, Result<(), AssetWriterError>> { - Box::pin(async move { - let full_old_path = self.root_path.join(old_path); - let full_new_path = self.root_path.join(new_path); - if let Some(parent) = full_new_path.parent() { - async_fs::create_dir_all(parent).await?; - } - async_fs::rename(full_old_path, full_new_path).await?; - Ok(()) - }) + ) -> Result<(), AssetWriterError> { + let full_old_path = self.root_path.join(old_path); + let full_new_path = self.root_path.join(new_path); + if let Some(parent) = full_new_path.parent() { + async_fs::create_dir_all(parent).await?; + } + async_fs::rename(full_old_path, full_new_path).await?; + Ok(()) } - fn rename_meta<'a>( + async fn rename_meta<'a>( &'a self, old_path: &'a Path, new_path: &'a Path, - ) -> BoxedFuture<'a, Result<(), AssetWriterError>> { - Box::pin(async move { - let old_meta_path = get_meta_path(old_path); - let new_meta_path = get_meta_path(new_path); - let full_old_path = self.root_path.join(old_meta_path); - let full_new_path = self.root_path.join(new_meta_path); - if let Some(parent) = full_new_path.parent() { - async_fs::create_dir_all(parent).await?; - } - async_fs::rename(full_old_path, full_new_path).await?; - Ok(()) - }) + ) -> Result<(), AssetWriterError> { + let old_meta_path = get_meta_path(old_path); + let new_meta_path = get_meta_path(new_path); + let full_old_path = self.root_path.join(old_meta_path); + let full_new_path = self.root_path.join(new_meta_path); + if let Some(parent) = full_new_path.parent() { + async_fs::create_dir_all(parent).await?; + } + async_fs::rename(full_old_path, full_new_path).await?; + Ok(()) } - fn remove_directory<'a>( - &'a self, - path: &'a Path, - ) -> BoxedFuture<'a, Result<(), AssetWriterError>> { - Box::pin(async move { - let full_path = self.root_path.join(path); - async_fs::remove_dir_all(full_path).await?; - Ok(()) - }) + async fn remove_directory<'a>(&'a self, path: &'a Path) -> Result<(), AssetWriterError> { + let full_path = self.root_path.join(path); + async_fs::remove_dir_all(full_path).await?; + Ok(()) } - fn remove_empty_directory<'a>( - &'a self, - path: &'a Path, - ) -> BoxedFuture<'a, Result<(), AssetWriterError>> { - Box::pin(async move { - let full_path = self.root_path.join(path); - async_fs::remove_dir(full_path).await?; - Ok(()) - }) + async fn remove_empty_directory<'a>(&'a self, path: &'a Path) -> Result<(), AssetWriterError> { + let full_path = self.root_path.join(path); + async_fs::remove_dir(full_path).await?; + Ok(()) } - fn remove_assets_in_directory<'a>( + async fn remove_assets_in_directory<'a>( &'a self, path: &'a Path, - ) -> BoxedFuture<'a, Result<(), AssetWriterError>> { - Box::pin(async move { - let full_path = self.root_path.join(path); - async_fs::remove_dir_all(&full_path).await?; - async_fs::create_dir_all(&full_path).await?; - Ok(()) - }) + ) -> Result<(), AssetWriterError> { + let full_path = self.root_path.join(path); + async_fs::remove_dir_all(&full_path).await?; + async_fs::create_dir_all(&full_path).await?; + Ok(()) } } diff --git a/crates/bevy_asset/src/io/file/sync_file_asset.rs b/crates/bevy_asset/src/io/file/sync_file_asset.rs index a8bf573a7ab071..426472150167ee 100644 --- a/crates/bevy_asset/src/io/file/sync_file_asset.rs +++ b/crates/bevy_asset/src/io/file/sync_file_asset.rs @@ -5,7 +5,6 @@ use crate::io::{ get_meta_path, AssetReader, AssetReaderError, AssetWriter, AssetWriterError, PathStream, Reader, Writer, }; -use bevy_utils::BoxedFuture; use std::{ fs::{read_dir, File}, @@ -76,221 +75,180 @@ impl Stream for DirReader { } impl AssetReader for FileAssetReader { - fn read<'a>( - &'a self, - path: &'a Path, - ) -> BoxedFuture<'a, Result>, AssetReaderError>> { - Box::pin(async move { - let full_path = self.root_path.join(path); - match File::open(&full_path) { - Ok(file) => { - let reader: Box = Box::new(FileReader(file)); - Ok(reader) - } - Err(e) => { - if e.kind() == std::io::ErrorKind::NotFound { - Err(AssetReaderError::NotFound(full_path)) - } else { - Err(e.into()) - } + async fn read<'a>(&'a self, path: &'a Path) -> Result>, AssetReaderError> { + let full_path = self.root_path.join(path); + match File::open(&full_path) { + Ok(file) => { + let reader: Box = Box::new(FileReader(file)); + Ok(reader) + } + Err(e) => { + if e.kind() == std::io::ErrorKind::NotFound { + Err(AssetReaderError::NotFound(full_path)) + } else { + Err(e.into()) } } - }) + } } - fn read_meta<'a>( - &'a self, - path: &'a Path, - ) -> BoxedFuture<'a, Result>, AssetReaderError>> { + async fn read_meta<'a>(&'a self, path: &'a Path) -> Result>, AssetReaderError> { let meta_path = get_meta_path(path); - Box::pin(async move { - let full_path = self.root_path.join(meta_path); - match File::open(&full_path) { - Ok(file) => { - let reader: Box = Box::new(FileReader(file)); - Ok(reader) - } - Err(e) => { - if e.kind() == std::io::ErrorKind::NotFound { - Err(AssetReaderError::NotFound(full_path)) - } else { - Err(e.into()) - } + let full_path = self.root_path.join(meta_path); + match File::open(&full_path) { + Ok(file) => { + let reader: Box = Box::new(FileReader(file)); + Ok(reader) + } + Err(e) => { + if e.kind() == std::io::ErrorKind::NotFound { + Err(AssetReaderError::NotFound(full_path)) + } else { + Err(e.into()) } } - }) + } } - fn read_directory<'a>( + async fn read_directory<'a>( &'a self, path: &'a Path, - ) -> BoxedFuture<'a, Result, AssetReaderError>> { - Box::pin(async move { - let full_path = self.root_path.join(path); - match read_dir(&full_path) { - Ok(read_dir) => { - let root_path = self.root_path.clone(); - let mapped_stream = read_dir.filter_map(move |f| { - f.ok().and_then(|dir_entry| { - let path = dir_entry.path(); - // filter out meta files as they are not considered assets - if let Some(ext) = path.extension().and_then(|e| e.to_str()) { - if ext.eq_ignore_ascii_case("meta") { - return None; - } + ) -> Result, AssetReaderError> { + let full_path = self.root_path.join(path); + match read_dir(&full_path) { + Ok(read_dir) => { + let root_path = self.root_path.clone(); + let mapped_stream = read_dir.filter_map(move |f| { + f.ok().and_then(|dir_entry| { + let path = dir_entry.path(); + // filter out meta files as they are not considered assets + if let Some(ext) = path.extension().and_then(|e| e.to_str()) { + if ext.eq_ignore_ascii_case("meta") { + return None; } - let relative_path = path.strip_prefix(&root_path).unwrap(); - Some(relative_path.to_owned()) - }) - }); - let read_dir: Box = Box::new(DirReader(mapped_stream.collect())); - Ok(read_dir) - } - Err(e) => { - if e.kind() == std::io::ErrorKind::NotFound { - Err(AssetReaderError::NotFound(full_path)) - } else { - Err(e.into()) - } + } + let relative_path = path.strip_prefix(&root_path).unwrap(); + Some(relative_path.to_owned()) + }) + }); + let read_dir: Box = Box::new(DirReader(mapped_stream.collect())); + Ok(read_dir) + } + Err(e) => { + if e.kind() == std::io::ErrorKind::NotFound { + Err(AssetReaderError::NotFound(full_path)) + } else { + Err(e.into()) } } - }) + } } - fn is_directory<'a>( + async fn is_directory<'a>( &'a self, path: &'a Path, - ) -> BoxedFuture<'a, std::result::Result> { - Box::pin(async move { - let full_path = self.root_path.join(path); - let metadata = full_path - .metadata() - .map_err(|_e| AssetReaderError::NotFound(path.to_owned()))?; - Ok(metadata.file_type().is_dir()) - }) + ) -> std::result::Result { + let full_path = self.root_path.join(path); + let metadata = full_path + .metadata() + .map_err(|_e| AssetReaderError::NotFound(path.to_owned()))?; + Ok(metadata.file_type().is_dir()) } } impl AssetWriter for FileAssetWriter { - fn write<'a>( - &'a self, - path: &'a Path, - ) -> BoxedFuture<'a, Result, AssetWriterError>> { - Box::pin(async move { - let full_path = self.root_path.join(path); - if let Some(parent) = full_path.parent() { - std::fs::create_dir_all(parent)?; - } - let file = File::create(&full_path)?; - let writer: Box = Box::new(FileWriter(file)); - Ok(writer) - }) + async fn write<'a>(&'a self, path: &'a Path) -> Result, AssetWriterError> { + let full_path = self.root_path.join(path); + if let Some(parent) = full_path.parent() { + std::fs::create_dir_all(parent)?; + } + let file = File::create(&full_path)?; + let writer: Box = Box::new(FileWriter(file)); + Ok(writer) } - fn write_meta<'a>( - &'a self, - path: &'a Path, - ) -> BoxedFuture<'a, Result, AssetWriterError>> { - Box::pin(async move { - let meta_path = get_meta_path(path); - let full_path = self.root_path.join(meta_path); - if let Some(parent) = full_path.parent() { - std::fs::create_dir_all(parent)?; - } - let file = File::create(&full_path)?; - let writer: Box = Box::new(FileWriter(file)); - Ok(writer) - }) + async fn write_meta<'a>(&'a self, path: &'a Path) -> Result, AssetWriterError> { + let meta_path = get_meta_path(path); + let full_path = self.root_path.join(meta_path); + if let Some(parent) = full_path.parent() { + std::fs::create_dir_all(parent)?; + } + let file = File::create(&full_path)?; + let writer: Box = Box::new(FileWriter(file)); + Ok(writer) } - fn remove<'a>( - &'a self, - path: &'a Path, - ) -> BoxedFuture<'a, std::result::Result<(), AssetWriterError>> { - Box::pin(async move { - let full_path = self.root_path.join(path); - std::fs::remove_file(full_path)?; - Ok(()) - }) + async fn remove<'a>(&'a self, path: &'a Path) -> std::result::Result<(), AssetWriterError> { + let full_path = self.root_path.join(path); + std::fs::remove_file(full_path)?; + Ok(()) } - fn remove_meta<'a>( + async fn remove_meta<'a>( &'a self, path: &'a Path, - ) -> BoxedFuture<'a, std::result::Result<(), AssetWriterError>> { - Box::pin(async move { - let meta_path = get_meta_path(path); - let full_path = self.root_path.join(meta_path); - std::fs::remove_file(full_path)?; - Ok(()) - }) + ) -> std::result::Result<(), AssetWriterError> { + let meta_path = get_meta_path(path); + let full_path = self.root_path.join(meta_path); + std::fs::remove_file(full_path)?; + Ok(()) } - fn remove_directory<'a>( + async fn remove_directory<'a>( &'a self, path: &'a Path, - ) -> BoxedFuture<'a, std::result::Result<(), AssetWriterError>> { - Box::pin(async move { - let full_path = self.root_path.join(path); - std::fs::remove_dir_all(full_path)?; - Ok(()) - }) + ) -> std::result::Result<(), AssetWriterError> { + let full_path = self.root_path.join(path); + std::fs::remove_dir_all(full_path)?; + Ok(()) } - fn remove_empty_directory<'a>( + async fn remove_empty_directory<'a>( &'a self, path: &'a Path, - ) -> BoxedFuture<'a, std::result::Result<(), AssetWriterError>> { - Box::pin(async move { - let full_path = self.root_path.join(path); - std::fs::remove_dir(full_path)?; - Ok(()) - }) + ) -> std::result::Result<(), AssetWriterError> { + let full_path = self.root_path.join(path); + std::fs::remove_dir(full_path)?; + Ok(()) } - fn remove_assets_in_directory<'a>( + async fn remove_assets_in_directory<'a>( &'a self, path: &'a Path, - ) -> BoxedFuture<'a, std::result::Result<(), AssetWriterError>> { - Box::pin(async move { - let full_path = self.root_path.join(path); - std::fs::remove_dir_all(&full_path)?; - std::fs::create_dir_all(&full_path)?; - Ok(()) - }) + ) -> std::result::Result<(), AssetWriterError> { + let full_path = self.root_path.join(path); + std::fs::remove_dir_all(&full_path)?; + std::fs::create_dir_all(&full_path)?; + Ok(()) } - fn rename<'a>( + async fn rename<'a>( &'a self, old_path: &'a Path, new_path: &'a Path, - ) -> BoxedFuture<'a, std::result::Result<(), AssetWriterError>> { - Box::pin(async move { - let full_old_path = self.root_path.join(old_path); - let full_new_path = self.root_path.join(new_path); - if let Some(parent) = full_new_path.parent() { - std::fs::create_dir_all(parent)?; - } - std::fs::rename(full_old_path, full_new_path)?; - Ok(()) - }) + ) -> std::result::Result<(), AssetWriterError> { + let full_old_path = self.root_path.join(old_path); + let full_new_path = self.root_path.join(new_path); + if let Some(parent) = full_new_path.parent() { + std::fs::create_dir_all(parent)?; + } + std::fs::rename(full_old_path, full_new_path)?; + Ok(()) } - fn rename_meta<'a>( + async fn rename_meta<'a>( &'a self, old_path: &'a Path, new_path: &'a Path, - ) -> BoxedFuture<'a, std::result::Result<(), AssetWriterError>> { - Box::pin(async move { - let old_meta_path = get_meta_path(old_path); - let new_meta_path = get_meta_path(new_path); - let full_old_path = self.root_path.join(old_meta_path); - let full_new_path = self.root_path.join(new_meta_path); - if let Some(parent) = full_new_path.parent() { - std::fs::create_dir_all(parent)?; - } - std::fs::rename(full_old_path, full_new_path)?; - Ok(()) - }) + ) -> std::result::Result<(), AssetWriterError> { + let old_meta_path = get_meta_path(old_path); + let new_meta_path = get_meta_path(new_path); + let full_old_path = self.root_path.join(old_meta_path); + let full_new_path = self.root_path.join(new_meta_path); + if let Some(parent) = full_new_path.parent() { + std::fs::create_dir_all(parent)?; + } + std::fs::rename(full_old_path, full_new_path)?; + Ok(()) } } diff --git a/crates/bevy_asset/src/io/gated.rs b/crates/bevy_asset/src/io/gated.rs index 76f531a04c88a0..d3d2b35f1f066b 100644 --- a/crates/bevy_asset/src/io/gated.rs +++ b/crates/bevy_asset/src/io/gated.rs @@ -1,5 +1,5 @@ use crate::io::{AssetReader, AssetReaderError, PathStream, Reader}; -use bevy_utils::{BoxedFuture, HashMap}; +use bevy_utils::HashMap; use crossbeam_channel::{Receiver, Sender}; use parking_lot::RwLock; use std::{path::Path, sync::Arc}; @@ -55,10 +55,7 @@ impl GatedReader { } impl AssetReader for GatedReader { - fn read<'a>( - &'a self, - path: &'a Path, - ) -> BoxedFuture<'a, Result>, AssetReaderError>> { + async fn read<'a>(&'a self, path: &'a Path) -> Result>, AssetReaderError> { let receiver = { let mut gates = self.gates.write(); let gates = gates @@ -66,31 +63,23 @@ impl AssetReader for GatedReader { .or_insert_with(crossbeam_channel::unbounded); gates.1.clone() }; - Box::pin(async move { - receiver.recv().unwrap(); - let result = self.reader.read(path).await?; - Ok(result) - }) + receiver.recv().unwrap(); + let result = self.reader.read(path).await?; + Ok(result) } - fn read_meta<'a>( - &'a self, - path: &'a Path, - ) -> BoxedFuture<'a, Result>, AssetReaderError>> { - self.reader.read_meta(path) + async fn read_meta<'a>(&'a self, path: &'a Path) -> Result>, AssetReaderError> { + self.reader.read_meta(path).await } - fn read_directory<'a>( + async fn read_directory<'a>( &'a self, path: &'a Path, - ) -> BoxedFuture<'a, Result, AssetReaderError>> { - self.reader.read_directory(path) + ) -> Result, AssetReaderError> { + self.reader.read_directory(path).await } - fn is_directory<'a>( - &'a self, - path: &'a Path, - ) -> BoxedFuture<'a, Result> { - self.reader.is_directory(path) + async fn is_directory<'a>(&'a self, path: &'a Path) -> Result { + self.reader.is_directory(path).await } } diff --git a/crates/bevy_asset/src/io/memory.rs b/crates/bevy_asset/src/io/memory.rs index cc13d0482056a3..563086f7b06203 100644 --- a/crates/bevy_asset/src/io/memory.rs +++ b/crates/bevy_asset/src/io/memory.rs @@ -1,5 +1,5 @@ use crate::io::{AssetReader, AssetReaderError, PathStream, Reader}; -use bevy_utils::{BoxedFuture, HashMap}; +use bevy_utils::HashMap; use futures_io::AsyncRead; use futures_lite::{ready, Stream}; use parking_lot::RwLock; @@ -237,62 +237,47 @@ impl AsyncRead for DataReader { } impl AssetReader for MemoryAssetReader { - fn read<'a>( - &'a self, - path: &'a Path, - ) -> BoxedFuture<'a, Result>, AssetReaderError>> { - Box::pin(async move { - self.root - .get_asset(path) - .map(|data| { - let reader: Box = Box::new(DataReader { - data, - bytes_read: 0, - }); - reader - }) - .ok_or_else(|| AssetReaderError::NotFound(path.to_path_buf())) - }) + async fn read<'a>(&'a self, path: &'a Path) -> Result>, AssetReaderError> { + self.root + .get_asset(path) + .map(|data| { + let reader: Box = Box::new(DataReader { + data, + bytes_read: 0, + }); + reader + }) + .ok_or_else(|| AssetReaderError::NotFound(path.to_path_buf())) } - fn read_meta<'a>( - &'a self, - path: &'a Path, - ) -> BoxedFuture<'a, Result>, AssetReaderError>> { - Box::pin(async move { - self.root - .get_metadata(path) - .map(|data| { - let reader: Box = Box::new(DataReader { - data, - bytes_read: 0, - }); - reader - }) - .ok_or_else(|| AssetReaderError::NotFound(path.to_path_buf())) - }) + async fn read_meta<'a>(&'a self, path: &'a Path) -> Result>, AssetReaderError> { + self.root + .get_metadata(path) + .map(|data| { + let reader: Box = Box::new(DataReader { + data, + bytes_read: 0, + }); + reader + }) + .ok_or_else(|| AssetReaderError::NotFound(path.to_path_buf())) } - fn read_directory<'a>( + async fn read_directory<'a>( &'a self, path: &'a Path, - ) -> BoxedFuture<'a, Result, AssetReaderError>> { - Box::pin(async move { - self.root - .get_dir(path) - .map(|dir| { - let stream: Box = Box::new(DirStream::new(dir)); - stream - }) - .ok_or_else(|| AssetReaderError::NotFound(path.to_path_buf())) - }) + ) -> Result, AssetReaderError> { + self.root + .get_dir(path) + .map(|dir| { + let stream: Box = Box::new(DirStream::new(dir)); + stream + }) + .ok_or_else(|| AssetReaderError::NotFound(path.to_path_buf())) } - fn is_directory<'a>( - &'a self, - path: &'a Path, - ) -> BoxedFuture<'a, Result> { - Box::pin(async move { Ok(self.root.get_dir(path).is_some()) }) + async fn is_directory<'a>(&'a self, path: &'a Path) -> Result { + Ok(self.root.get_dir(path).is_some()) } } diff --git a/crates/bevy_asset/src/io/mod.rs b/crates/bevy_asset/src/io/mod.rs index 9b8f83b0eea7fe..766e536455fa70 100644 --- a/crates/bevy_asset/src/io/mod.rs +++ b/crates/bevy_asset/src/io/mod.rs @@ -21,7 +21,7 @@ mod source; pub use futures_lite::{AsyncReadExt, AsyncWriteExt}; pub use source::*; -use bevy_utils::BoxedFuture; +use bevy_utils::{BoxedFuture, ConditionalSendFuture}; use futures_io::{AsyncRead, AsyncWrite}; use futures_lite::{ready, Stream}; use std::{ @@ -59,7 +59,7 @@ pub type Reader<'a> = dyn AsyncRead + Unpin + Send + Sync + 'a; /// Performs read operations on an asset storage. [`AssetReader`] exposes a "virtual filesystem" /// API, where asset bytes and asset metadata bytes are both stored and accessible for a given -/// `path`. +/// `path`. This trait is not object safe, if needed use a dyn [`ErasedAssetReader`] instead. /// /// Also see [`AssetWriter`]. pub trait AssetReader: Send + Sync + 'static { @@ -67,35 +67,90 @@ pub trait AssetReader: Send + Sync + 'static { fn read<'a>( &'a self, path: &'a Path, - ) -> BoxedFuture<'a, Result>, AssetReaderError>>; + ) -> impl ConditionalSendFuture>, AssetReaderError>>; /// Returns a future to load the full file data at the provided path. fn read_meta<'a>( &'a self, path: &'a Path, - ) -> BoxedFuture<'a, Result>, AssetReaderError>>; + ) -> impl ConditionalSendFuture>, AssetReaderError>>; /// Returns an iterator of directory entry names at the provided path. fn read_directory<'a>( &'a self, path: &'a Path, - ) -> BoxedFuture<'a, Result, AssetReaderError>>; - /// Returns true if the provided path points to a directory. + ) -> impl ConditionalSendFuture, AssetReaderError>>; + /// Returns an iterator of directory entry names at the provided path. fn is_directory<'a>( &'a self, path: &'a Path, - ) -> BoxedFuture<'a, Result>; - + ) -> impl ConditionalSendFuture>; /// Reads asset metadata bytes at the given `path` into a [`Vec`]. This is a convenience /// function that wraps [`AssetReader::read_meta`] by default. fn read_meta_bytes<'a>( &'a self, path: &'a Path, - ) -> BoxedFuture<'a, Result, AssetReaderError>> { - Box::pin(async move { + ) -> impl ConditionalSendFuture, AssetReaderError>> { + async { let mut meta_reader = self.read_meta(path).await?; let mut meta_bytes = Vec::new(); meta_reader.read_to_end(&mut meta_bytes).await?; Ok(meta_bytes) - }) + } + } +} + +/// Equivalent to an [`AssetReader`] but using boxed futures, necessary eg. when using a `dyn AssetReader`, +/// as [`AssetReader`] isn't currently object safe. +pub trait ErasedAssetReader: Send + Sync + 'static { + /// Returns a future to load the full file data at the provided path. + fn read<'a>(&'a self, path: &'a Path) + -> BoxedFuture>, AssetReaderError>>; + /// Returns a future to load the full file data at the provided path. + fn read_meta<'a>( + &'a self, + path: &'a Path, + ) -> BoxedFuture>, AssetReaderError>>; + /// Returns an iterator of directory entry names at the provided path. + fn read_directory<'a>( + &'a self, + path: &'a Path, + ) -> BoxedFuture, AssetReaderError>>; + /// Returns true if the provided path points to a directory. + fn is_directory<'a>(&'a self, path: &'a Path) -> BoxedFuture>; + /// Reads asset metadata bytes at the given `path` into a [`Vec`]. This is a convenience + /// function that wraps [`ErasedAssetReader::read_meta`] by default. + fn read_meta_bytes<'a>( + &'a self, + path: &'a Path, + ) -> BoxedFuture, AssetReaderError>>; +} + +impl ErasedAssetReader for T { + fn read<'a>( + &'a self, + path: &'a Path, + ) -> BoxedFuture>, AssetReaderError>> { + Box::pin(Self::read(self, path)) + } + fn read_meta<'a>( + &'a self, + path: &'a Path, + ) -> BoxedFuture>, AssetReaderError>> { + Box::pin(Self::read_meta(self, path)) + } + fn read_directory<'a>( + &'a self, + path: &'a Path, + ) -> BoxedFuture, AssetReaderError>> { + Box::pin(Self::read_directory(self, path)) + } + fn is_directory<'a>(&'a self, path: &'a Path) -> BoxedFuture> { + Box::pin(Self::is_directory(self, path)) + } + fn read_meta_bytes<'a>( + &'a self, + path: &'a Path, + ) -> BoxedFuture, AssetReaderError>> { + Box::pin(Self::read_meta_bytes(self, path)) } } @@ -113,7 +168,7 @@ pub enum AssetWriterError { /// Preforms write operations on an asset storage. [`AssetWriter`] exposes a "virtual filesystem" /// API, where asset bytes and asset metadata bytes are both stored and accessible for a given -/// `path`. +/// `path`. This trait is not object safe, if needed use a dyn [`ErasedAssetWriter`] instead. /// /// Also see [`AssetReader`]. pub trait AssetWriter: Send + Sync + 'static { @@ -121,72 +176,195 @@ pub trait AssetWriter: Send + Sync + 'static { fn write<'a>( &'a self, path: &'a Path, - ) -> BoxedFuture<'a, Result, AssetWriterError>>; + ) -> impl ConditionalSendFuture, AssetWriterError>>; /// Writes the full asset meta bytes at the provided path. /// This _should not_ include storage specific extensions like `.meta`. fn write_meta<'a>( &'a self, path: &'a Path, - ) -> BoxedFuture<'a, Result, AssetWriterError>>; + ) -> impl ConditionalSendFuture, AssetWriterError>>; /// Removes the asset stored at the given path. - fn remove<'a>(&'a self, path: &'a Path) -> BoxedFuture<'a, Result<(), AssetWriterError>>; + fn remove<'a>( + &'a self, + path: &'a Path, + ) -> impl ConditionalSendFuture>; /// Removes the asset meta stored at the given path. /// This _should not_ include storage specific extensions like `.meta`. - fn remove_meta<'a>(&'a self, path: &'a Path) -> BoxedFuture<'a, Result<(), AssetWriterError>>; + fn remove_meta<'a>( + &'a self, + path: &'a Path, + ) -> impl ConditionalSendFuture>; /// Renames the asset at `old_path` to `new_path` fn rename<'a>( &'a self, old_path: &'a Path, new_path: &'a Path, - ) -> BoxedFuture<'a, Result<(), AssetWriterError>>; + ) -> impl ConditionalSendFuture>; /// Renames the asset meta for the asset at `old_path` to `new_path`. /// This _should not_ include storage specific extensions like `.meta`. fn rename_meta<'a>( &'a self, old_path: &'a Path, new_path: &'a Path, - ) -> BoxedFuture<'a, Result<(), AssetWriterError>>; + ) -> impl ConditionalSendFuture>; /// Removes the directory at the given path, including all assets _and_ directories in that directory. fn remove_directory<'a>( &'a self, path: &'a Path, - ) -> BoxedFuture<'a, Result<(), AssetWriterError>>; + ) -> impl ConditionalSendFuture>; /// Removes the directory at the given path, but only if it is completely empty. This will return an error if the /// directory is not empty. fn remove_empty_directory<'a>( &'a self, path: &'a Path, - ) -> BoxedFuture<'a, Result<(), AssetWriterError>>; + ) -> impl ConditionalSendFuture>; /// Removes all assets (and directories) in this directory, resulting in an empty directory. fn remove_assets_in_directory<'a>( &'a self, path: &'a Path, - ) -> BoxedFuture<'a, Result<(), AssetWriterError>>; + ) -> impl ConditionalSendFuture>; /// Writes the asset `bytes` to the given `path`. fn write_bytes<'a>( &'a self, path: &'a Path, bytes: &'a [u8], - ) -> BoxedFuture<'a, Result<(), AssetWriterError>> { - Box::pin(async move { + ) -> impl ConditionalSendFuture> { + async { let mut writer = self.write(path).await?; writer.write_all(bytes).await?; writer.flush().await?; Ok(()) - }) + } } /// Writes the asset meta `bytes` to the given `path`. fn write_meta_bytes<'a>( &'a self, path: &'a Path, bytes: &'a [u8], - ) -> BoxedFuture<'a, Result<(), AssetWriterError>> { - Box::pin(async move { + ) -> impl ConditionalSendFuture> { + async { let mut meta_writer = self.write_meta(path).await?; meta_writer.write_all(bytes).await?; meta_writer.flush().await?; Ok(()) - }) + } + } +} + +/// Equivalent to an [`AssetWriter`] but using boxed futures, necessary eg. when using a `dyn AssetWriter`, +/// as [`AssetWriter`] isn't currently object safe. +pub trait ErasedAssetWriter: Send + Sync + 'static { + /// Writes the full asset bytes at the provided path. + fn write<'a>(&'a self, path: &'a Path) -> BoxedFuture, AssetWriterError>>; + /// Writes the full asset meta bytes at the provided path. + /// This _should not_ include storage specific extensions like `.meta`. + fn write_meta<'a>( + &'a self, + path: &'a Path, + ) -> BoxedFuture, AssetWriterError>>; + /// Removes the asset stored at the given path. + fn remove<'a>(&'a self, path: &'a Path) -> BoxedFuture>; + /// Removes the asset meta stored at the given path. + /// This _should not_ include storage specific extensions like `.meta`. + fn remove_meta<'a>(&'a self, path: &'a Path) -> BoxedFuture>; + /// Renames the asset at `old_path` to `new_path` + fn rename<'a>( + &'a self, + old_path: &'a Path, + new_path: &'a Path, + ) -> BoxedFuture>; + /// Renames the asset meta for the asset at `old_path` to `new_path`. + /// This _should not_ include storage specific extensions like `.meta`. + fn rename_meta<'a>( + &'a self, + old_path: &'a Path, + new_path: &'a Path, + ) -> BoxedFuture>; + /// Removes the directory at the given path, including all assets _and_ directories in that directory. + fn remove_directory<'a>(&'a self, path: &'a Path) -> BoxedFuture>; + /// Removes the directory at the given path, but only if it is completely empty. This will return an error if the + /// directory is not empty. + fn remove_empty_directory<'a>( + &'a self, + path: &'a Path, + ) -> BoxedFuture>; + /// Removes all assets (and directories) in this directory, resulting in an empty directory. + fn remove_assets_in_directory<'a>( + &'a self, + path: &'a Path, + ) -> BoxedFuture>; + /// Writes the asset `bytes` to the given `path`. + fn write_bytes<'a>( + &'a self, + path: &'a Path, + bytes: &'a [u8], + ) -> BoxedFuture>; + /// Writes the asset meta `bytes` to the given `path`. + fn write_meta_bytes<'a>( + &'a self, + path: &'a Path, + bytes: &'a [u8], + ) -> BoxedFuture>; +} + +impl ErasedAssetWriter for T { + fn write<'a>(&'a self, path: &'a Path) -> BoxedFuture, AssetWriterError>> { + Box::pin(Self::write(self, path)) + } + fn write_meta<'a>( + &'a self, + path: &'a Path, + ) -> BoxedFuture, AssetWriterError>> { + Box::pin(Self::write_meta(self, path)) + } + fn remove<'a>(&'a self, path: &'a Path) -> BoxedFuture> { + Box::pin(Self::remove(self, path)) + } + fn remove_meta<'a>(&'a self, path: &'a Path) -> BoxedFuture> { + Box::pin(Self::remove_meta(self, path)) + } + fn rename<'a>( + &'a self, + old_path: &'a Path, + new_path: &'a Path, + ) -> BoxedFuture> { + Box::pin(Self::rename(self, old_path, new_path)) + } + fn rename_meta<'a>( + &'a self, + old_path: &'a Path, + new_path: &'a Path, + ) -> BoxedFuture> { + Box::pin(Self::rename_meta(self, old_path, new_path)) + } + fn remove_directory<'a>(&'a self, path: &'a Path) -> BoxedFuture> { + Box::pin(Self::remove_directory(self, path)) + } + fn remove_empty_directory<'a>( + &'a self, + path: &'a Path, + ) -> BoxedFuture> { + Box::pin(Self::remove_empty_directory(self, path)) + } + fn remove_assets_in_directory<'a>( + &'a self, + path: &'a Path, + ) -> BoxedFuture> { + Box::pin(Self::remove_assets_in_directory(self, path)) + } + fn write_bytes<'a>( + &'a self, + path: &'a Path, + bytes: &'a [u8], + ) -> BoxedFuture> { + Box::pin(Self::write_bytes(self, path, bytes)) + } + fn write_meta_bytes<'a>( + &'a self, + path: &'a Path, + bytes: &'a [u8], + ) -> BoxedFuture> { + Box::pin(Self::write_meta_bytes(self, path, bytes)) } } diff --git a/crates/bevy_asset/src/io/processor_gated.rs b/crates/bevy_asset/src/io/processor_gated.rs index b86460f04b6fd5..5f3bfe14678384 100644 --- a/crates/bevy_asset/src/io/processor_gated.rs +++ b/crates/bevy_asset/src/io/processor_gated.rs @@ -5,16 +5,17 @@ use crate::{ }; use async_lock::RwLockReadGuardArc; use bevy_utils::tracing::trace; -use bevy_utils::BoxedFuture; use futures_io::AsyncRead; use std::{path::Path, pin::Pin, sync::Arc}; +use super::ErasedAssetReader; + /// An [`AssetReader`] that will prevent asset (and asset metadata) read futures from returning for a /// given path until that path has been processed by [`AssetProcessor`]. /// /// [`AssetProcessor`]: crate::processor::AssetProcessor pub struct ProcessorGatedReader { - reader: Box, + reader: Box, source: AssetSourceId<'static>, processor_data: Arc, } @@ -23,7 +24,7 @@ impl ProcessorGatedReader { /// Creates a new [`ProcessorGatedReader`]. pub fn new( source: AssetSourceId<'static>, - reader: Box, + reader: Box, processor_data: Arc, ) -> Self { Self { @@ -48,87 +49,69 @@ impl ProcessorGatedReader { } impl AssetReader for ProcessorGatedReader { - fn read<'a>( - &'a self, - path: &'a Path, - ) -> BoxedFuture<'a, Result>, AssetReaderError>> { - Box::pin(async move { - let asset_path = AssetPath::from(path.to_path_buf()).with_source(self.source.clone()); - trace!("Waiting for processing to finish before reading {asset_path}"); - let process_result = self - .processor_data - .wait_until_processed(asset_path.clone()) - .await; - match process_result { - ProcessStatus::Processed => {} - ProcessStatus::Failed | ProcessStatus::NonExistent => { - return Err(AssetReaderError::NotFound(path.to_owned())); - } + async fn read<'a>(&'a self, path: &'a Path) -> Result>, AssetReaderError> { + let asset_path = AssetPath::from(path.to_path_buf()).with_source(self.source.clone()); + trace!("Waiting for processing to finish before reading {asset_path}"); + let process_result = self + .processor_data + .wait_until_processed(asset_path.clone()) + .await; + match process_result { + ProcessStatus::Processed => {} + ProcessStatus::Failed | ProcessStatus::NonExistent => { + return Err(AssetReaderError::NotFound(path.to_owned())); } - trace!("Processing finished with {asset_path}, reading {process_result:?}",); - let lock = self.get_transaction_lock(&asset_path).await?; - let asset_reader = self.reader.read(path).await?; - let reader: Box> = - Box::new(TransactionLockedReader::new(asset_reader, lock)); - Ok(reader) - }) + } + trace!("Processing finished with {asset_path}, reading {process_result:?}",); + let lock = self.get_transaction_lock(&asset_path).await?; + let asset_reader = self.reader.read(path).await?; + let reader: Box> = Box::new(TransactionLockedReader::new(asset_reader, lock)); + Ok(reader) } - fn read_meta<'a>( - &'a self, - path: &'a Path, - ) -> BoxedFuture<'a, Result>, AssetReaderError>> { - Box::pin(async move { - let asset_path = AssetPath::from(path.to_path_buf()).with_source(self.source.clone()); - trace!("Waiting for processing to finish before reading meta for {asset_path}",); - let process_result = self - .processor_data - .wait_until_processed(asset_path.clone()) - .await; - match process_result { - ProcessStatus::Processed => {} - ProcessStatus::Failed | ProcessStatus::NonExistent => { - return Err(AssetReaderError::NotFound(path.to_owned())); - } + async fn read_meta<'a>(&'a self, path: &'a Path) -> Result>, AssetReaderError> { + let asset_path = AssetPath::from(path.to_path_buf()).with_source(self.source.clone()); + trace!("Waiting for processing to finish before reading meta for {asset_path}",); + let process_result = self + .processor_data + .wait_until_processed(asset_path.clone()) + .await; + match process_result { + ProcessStatus::Processed => {} + ProcessStatus::Failed | ProcessStatus::NonExistent => { + return Err(AssetReaderError::NotFound(path.to_owned())); } - trace!("Processing finished with {process_result:?}, reading meta for {asset_path}",); - let lock = self.get_transaction_lock(&asset_path).await?; - let meta_reader = self.reader.read_meta(path).await?; - let reader: Box> = Box::new(TransactionLockedReader::new(meta_reader, lock)); - Ok(reader) - }) + } + trace!("Processing finished with {process_result:?}, reading meta for {asset_path}",); + let lock = self.get_transaction_lock(&asset_path).await?; + let meta_reader = self.reader.read_meta(path).await?; + let reader: Box> = Box::new(TransactionLockedReader::new(meta_reader, lock)); + Ok(reader) } - fn read_directory<'a>( + async fn read_directory<'a>( &'a self, path: &'a Path, - ) -> BoxedFuture<'a, Result, AssetReaderError>> { - Box::pin(async move { - trace!( - "Waiting for processing to finish before reading directory {:?}", - path - ); - self.processor_data.wait_until_finished().await; - trace!("Processing finished, reading directory {:?}", path); - let result = self.reader.read_directory(path).await?; - Ok(result) - }) + ) -> Result, AssetReaderError> { + trace!( + "Waiting for processing to finish before reading directory {:?}", + path + ); + self.processor_data.wait_until_finished().await; + trace!("Processing finished, reading directory {:?}", path); + let result = self.reader.read_directory(path).await?; + Ok(result) } - fn is_directory<'a>( - &'a self, - path: &'a Path, - ) -> BoxedFuture<'a, Result> { - Box::pin(async move { - trace!( - "Waiting for processing to finish before reading directory {:?}", - path - ); - self.processor_data.wait_until_finished().await; - trace!("Processing finished, getting directory status {:?}", path); - let result = self.reader.is_directory(path).await?; - Ok(result) - }) + async fn is_directory<'a>(&'a self, path: &'a Path) -> Result { + trace!( + "Waiting for processing to finish before reading directory {:?}", + path + ); + self.processor_data.wait_until_finished().await; + trace!("Processing finished, getting directory status {:?}", path); + let result = self.reader.is_directory(path).await?; + Ok(result) } } diff --git a/crates/bevy_asset/src/io/source.rs b/crates/bevy_asset/src/io/source.rs index 7293a73bea4183..21bd29294e31b8 100644 --- a/crates/bevy_asset/src/io/source.rs +++ b/crates/bevy_asset/src/io/source.rs @@ -1,8 +1,5 @@ use crate::{ - io::{ - processor_gated::ProcessorGatedReader, AssetReader, AssetSourceEvent, AssetWatcher, - AssetWriter, - }, + io::{processor_gated::ProcessorGatedReader, AssetSourceEvent, AssetWatcher}, processor::AssetProcessorData, }; use bevy_ecs::system::Resource; @@ -11,6 +8,12 @@ use bevy_utils::{CowArc, Duration, HashMap}; use std::{fmt::Display, hash::Hash, sync::Arc}; use thiserror::Error; +use super::{ErasedAssetReader, ErasedAssetWriter}; + +// Needed for doc strings. +#[allow(unused_imports)] +use crate::io::{AssetReader, AssetWriter}; + /// A reference to an "asset source", which maps to an [`AssetReader`] and/or [`AssetWriter`]. /// /// * [`AssetSourceId::Default`] corresponds to "default asset paths" that don't specify a source: `/path/to/asset.png` @@ -110,8 +113,8 @@ impl<'a> PartialEq for AssetSourceId<'a> { /// and whether or not the source is processed. #[derive(Default)] pub struct AssetSourceBuilder { - pub reader: Option Box + Send + Sync>>, - pub writer: Option Option> + Send + Sync>>, + pub reader: Option Box + Send + Sync>>, + pub writer: Option Option> + Send + Sync>>, pub watcher: Option< Box< dyn FnMut(crossbeam_channel::Sender) -> Option> @@ -119,9 +122,9 @@ pub struct AssetSourceBuilder { + Sync, >, >, - pub processed_reader: Option Box + Send + Sync>>, + pub processed_reader: Option Box + Send + Sync>>, pub processed_writer: - Option Option> + Send + Sync>>, + Option Option> + Send + Sync>>, pub processed_watcher: Option< Box< dyn FnMut(crossbeam_channel::Sender) -> Option> @@ -192,7 +195,7 @@ impl AssetSourceBuilder { /// Will use the given `reader` function to construct unprocessed [`AssetReader`] instances. pub fn with_reader( mut self, - reader: impl FnMut() -> Box + Send + Sync + 'static, + reader: impl FnMut() -> Box + Send + Sync + 'static, ) -> Self { self.reader = Some(Box::new(reader)); self @@ -201,7 +204,7 @@ impl AssetSourceBuilder { /// Will use the given `writer` function to construct unprocessed [`AssetWriter`] instances. pub fn with_writer( mut self, - writer: impl FnMut(bool) -> Option> + Send + Sync + 'static, + writer: impl FnMut(bool) -> Option> + Send + Sync + 'static, ) -> Self { self.writer = Some(Box::new(writer)); self @@ -222,7 +225,7 @@ impl AssetSourceBuilder { /// Will use the given `reader` function to construct processed [`AssetReader`] instances. pub fn with_processed_reader( mut self, - reader: impl FnMut() -> Box + Send + Sync + 'static, + reader: impl FnMut() -> Box + Send + Sync + 'static, ) -> Self { self.processed_reader = Some(Box::new(reader)); self @@ -231,7 +234,7 @@ impl AssetSourceBuilder { /// Will use the given `writer` function to construct processed [`AssetWriter`] instances. pub fn with_processed_writer( mut self, - writer: impl FnMut(bool) -> Option> + Send + Sync + 'static, + writer: impl FnMut(bool) -> Option> + Send + Sync + 'static, ) -> Self { self.processed_writer = Some(Box::new(writer)); self @@ -355,10 +358,10 @@ impl AssetSourceBuilders { /// for a specific asset source, identified by an [`AssetSourceId`]. pub struct AssetSource { id: AssetSourceId<'static>, - reader: Box, - writer: Option>, - processed_reader: Option>, - processed_writer: Option>, + reader: Box, + writer: Option>, + processed_reader: Option>, + processed_writer: Option>, watcher: Option>, processed_watcher: Option>, event_receiver: Option>, @@ -379,13 +382,13 @@ impl AssetSource { /// Return's this source's unprocessed [`AssetReader`]. #[inline] - pub fn reader(&self) -> &dyn AssetReader { + pub fn reader(&self) -> &dyn ErasedAssetReader { &*self.reader } /// Return's this source's unprocessed [`AssetWriter`], if it exists. #[inline] - pub fn writer(&self) -> Result<&dyn AssetWriter, MissingAssetWriterError> { + pub fn writer(&self) -> Result<&dyn ErasedAssetWriter, MissingAssetWriterError> { self.writer .as_deref() .ok_or_else(|| MissingAssetWriterError(self.id.clone_owned())) @@ -393,7 +396,9 @@ impl AssetSource { /// Return's this source's processed [`AssetReader`], if it exists. #[inline] - pub fn processed_reader(&self) -> Result<&dyn AssetReader, MissingProcessedAssetReaderError> { + pub fn processed_reader( + &self, + ) -> Result<&dyn ErasedAssetReader, MissingProcessedAssetReaderError> { self.processed_reader .as_deref() .ok_or_else(|| MissingProcessedAssetReaderError(self.id.clone_owned())) @@ -401,7 +406,9 @@ impl AssetSource { /// Return's this source's processed [`AssetWriter`], if it exists. #[inline] - pub fn processed_writer(&self) -> Result<&dyn AssetWriter, MissingProcessedAssetWriterError> { + pub fn processed_writer( + &self, + ) -> Result<&dyn ErasedAssetWriter, MissingProcessedAssetWriterError> { self.processed_writer .as_deref() .ok_or_else(|| MissingProcessedAssetWriterError(self.id.clone_owned())) @@ -429,7 +436,9 @@ impl AssetSource { /// Returns a builder function for this platform's default [`AssetReader`]. `path` is the relative path to /// the asset root. - pub fn get_default_reader(_path: String) -> impl FnMut() -> Box + Send + Sync { + pub fn get_default_reader( + _path: String, + ) -> impl FnMut() -> Box + Send + Sync { move || { #[cfg(all(not(target_arch = "wasm32"), not(target_os = "android")))] return Box::new(super::file::FileAssetReader::new(&_path)); @@ -444,7 +453,7 @@ impl AssetSource { /// the asset root. This will return [`None`] if this platform does not support writing assets by default. pub fn get_default_writer( _path: String, - ) -> impl FnMut(bool) -> Option> + Send + Sync { + ) -> impl FnMut(bool) -> Option> + Send + Sync { move |_create_root: bool| { #[cfg(all(not(target_arch = "wasm32"), not(target_os = "android")))] return Some(Box::new(super::file::FileAssetWriter::new( diff --git a/crates/bevy_asset/src/io/wasm.rs b/crates/bevy_asset/src/io/wasm.rs index aab497ddfa7cc4..95f490550815ce 100644 --- a/crates/bevy_asset/src/io/wasm.rs +++ b/crates/bevy_asset/src/io/wasm.rs @@ -2,7 +2,6 @@ use crate::io::{ get_meta_path, AssetReader, AssetReaderError, EmptyPathStream, PathStream, Reader, VecReader, }; use bevy_utils::tracing::error; -use bevy_utils::BoxedFuture; use js_sys::{Uint8Array, JSON}; use std::path::{Path, PathBuf}; use wasm_bindgen::{JsCast, JsValue}; @@ -59,40 +58,30 @@ impl HttpWasmAssetReader { } impl AssetReader for HttpWasmAssetReader { - fn read<'a>( - &'a self, - path: &'a Path, - ) -> BoxedFuture<'a, Result>, AssetReaderError>> { - Box::pin(async move { - let path = self.root_path.join(path); - self.fetch_bytes(path).await - }) + async fn read<'a>(&'a self, path: &'a Path) -> Result>, AssetReaderError> { + let path = self.root_path.join(path); + self.fetch_bytes(path).await } - fn read_meta<'a>( - &'a self, - path: &'a Path, - ) -> BoxedFuture<'a, Result>, AssetReaderError>> { - Box::pin(async move { - let meta_path = get_meta_path(&self.root_path.join(path)); - Ok(self.fetch_bytes(meta_path).await?) - }) + async fn read_meta<'a>(&'a self, path: &'a Path) -> Result>, AssetReaderError> { + let meta_path = get_meta_path(&self.root_path.join(path)); + Ok(self.fetch_bytes(meta_path).await?) } - fn read_directory<'a>( + async fn read_directory<'a>( &'a self, _path: &'a Path, - ) -> BoxedFuture<'a, Result, AssetReaderError>> { + ) -> Result, AssetReaderError> { let stream: Box = Box::new(EmptyPathStream); error!("Reading directories is not supported with the HttpWasmAssetReader"); - Box::pin(async move { Ok(stream) }) + Ok(stream) } - fn is_directory<'a>( + async fn is_directory<'a>( &'a self, _path: &'a Path, - ) -> BoxedFuture<'a, std::result::Result> { + ) -> std::result::Result { error!("Reading directories is not supported with the HttpWasmAssetReader"); - Box::pin(async move { Ok(false) }) + Ok(false) } } diff --git a/crates/bevy_asset/src/lib.rs b/crates/bevy_asset/src/lib.rs index 4e579a04f2597c..f15797f619402b 100644 --- a/crates/bevy_asset/src/lib.rs +++ b/crates/bevy_asset/src/lib.rs @@ -40,8 +40,6 @@ pub use path::*; pub use reflect::*; pub use server::*; -pub use bevy_utils::BoxedFuture; - /// Rusty Object Notation, a crate used to serialize and deserialize bevy assets. pub use ron; @@ -448,7 +446,7 @@ mod tests { }; use bevy_log::LogPlugin; use bevy_reflect::TypePath; - use bevy_utils::{BoxedFuture, Duration, HashMap}; + use bevy_utils::{Duration, HashMap}; use futures_lite::AsyncReadExt; use serde::{Deserialize, Serialize}; use std::{path::Path, sync::Arc}; @@ -497,40 +495,38 @@ mod tests { type Error = CoolTextLoaderError; - fn load<'a>( + async fn load<'a>( &'a self, - reader: &'a mut Reader, + reader: &'a mut Reader<'_>, _settings: &'a Self::Settings, - load_context: &'a mut LoadContext, - ) -> BoxedFuture<'a, Result> { - Box::pin(async move { - let mut bytes = Vec::new(); - reader.read_to_end(&mut bytes).await?; - let mut ron: CoolTextRon = ron::de::from_bytes(&bytes)?; - let mut embedded = String::new(); - for dep in ron.embedded_dependencies { - let loaded = load_context.load_direct(&dep).await.map_err(|_| { - Self::Error::CannotLoadDependency { - dependency: dep.into(), - } - })?; - let cool = loaded.get::().unwrap(); - embedded.push_str(&cool.text); - } - Ok(CoolText { - text: ron.text, - embedded, - dependencies: ron - .dependencies - .iter() - .map(|p| load_context.load(p)) - .collect(), - sub_texts: ron - .sub_texts - .drain(..) - .map(|text| load_context.add_labeled_asset(text.clone(), SubText { text })) - .collect(), - }) + load_context: &'a mut LoadContext<'_>, + ) -> Result { + let mut bytes = Vec::new(); + reader.read_to_end(&mut bytes).await?; + let mut ron: CoolTextRon = ron::de::from_bytes(&bytes)?; + let mut embedded = String::new(); + for dep in ron.embedded_dependencies { + let loaded = load_context.load_direct(&dep).await.map_err(|_| { + Self::Error::CannotLoadDependency { + dependency: dep.into(), + } + })?; + let cool = loaded.get::().unwrap(); + embedded.push_str(&cool.text); + } + Ok(CoolText { + text: ron.text, + embedded, + dependencies: ron + .dependencies + .iter() + .map(|p| load_context.load(p)) + .collect(), + sub_texts: ron + .sub_texts + .drain(..) + .map(|text| load_context.add_labeled_asset(text.clone(), SubText { text })) + .collect(), }) } @@ -560,31 +556,25 @@ mod tests { } impl AssetReader for UnstableMemoryAssetReader { - fn is_directory<'a>( - &'a self, - path: &'a Path, - ) -> BoxedFuture<'a, Result> { - self.memory_reader.is_directory(path) + async fn is_directory<'a>(&'a self, path: &'a Path) -> Result { + self.memory_reader.is_directory(path).await } - fn read_directory<'a>( + async fn read_directory<'a>( &'a self, path: &'a Path, - ) -> BoxedFuture<'a, Result, AssetReaderError>> { - self.memory_reader.read_directory(path) + ) -> Result, AssetReaderError> { + self.memory_reader.read_directory(path).await } - fn read_meta<'a>( + async fn read_meta<'a>( &'a self, path: &'a Path, - ) -> BoxedFuture<'a, Result>, AssetReaderError>> { - self.memory_reader.read_meta(path) + ) -> Result>, AssetReaderError> { + self.memory_reader.read_meta(path).await } - fn read<'a>( + async fn read<'a>( &'a self, path: &'a Path, - ) -> BoxedFuture< - 'a, - Result>, bevy_asset::io::AssetReaderError>, - > { + ) -> Result>, bevy_asset::io::AssetReaderError> { let attempt_number = { let mut attempt_counters = self.attempt_counters.lock().unwrap(); if let Some(existing) = attempt_counters.get_mut(path) { @@ -605,13 +595,14 @@ mod tests { ), ); let wait = self.load_delay; - return Box::pin(async move { + return async move { std::thread::sleep(wait); Err(AssetReaderError::Io(io_error.into())) - }); + } + .await; } - self.memory_reader.read(path) + self.memory_reader.read(path).await } } diff --git a/crates/bevy_asset/src/loader.rs b/crates/bevy_asset/src/loader.rs index 627b286482a5d7..ae3f08511adbf4 100644 --- a/crates/bevy_asset/src/loader.rs +++ b/crates/bevy_asset/src/loader.rs @@ -9,7 +9,7 @@ use crate::{ UntypedAssetId, UntypedHandle, }; use bevy_ecs::world::World; -use bevy_utils::{BoxedFuture, CowArc, HashMap, HashSet}; +use bevy_utils::{BoxedFuture, ConditionalSendFuture, CowArc, HashMap, HashSet}; use downcast_rs::{impl_downcast, Downcast}; use futures_lite::AsyncReadExt; use ron::error::SpannedError; @@ -35,7 +35,7 @@ pub trait AssetLoader: Send + Sync + 'static { reader: &'a mut Reader, settings: &'a Self::Settings, load_context: &'a mut LoadContext, - ) -> BoxedFuture<'a, Result>; + ) -> impl ConditionalSendFuture>; /// Returns a list of extensions supported by this [`AssetLoader`], without the preceding dot. /// Note that users of this [`AssetLoader`] may choose to load files with a non-matching extension. diff --git a/crates/bevy_asset/src/meta.rs b/crates/bevy_asset/src/meta.rs index 2b082e95505892..ccc1df9b727ca0 100644 --- a/crates/bevy_asset/src/meta.rs +++ b/crates/bevy_asset/src/meta.rs @@ -171,12 +171,12 @@ impl Process for () { type Settings = (); type OutputLoader = (); - fn process<'a>( + async fn process<'a>( &'a self, - _context: &'a mut bevy_asset::processor::ProcessContext, + _context: &'a mut bevy_asset::processor::ProcessContext<'_>, _meta: AssetMeta<(), Self>, _writer: &'a mut bevy_asset::io::Writer, - ) -> bevy_utils::BoxedFuture<'a, Result<(), bevy_asset::processor::ProcessError>> { + ) -> Result<(), bevy_asset::processor::ProcessError> { unreachable!() } } @@ -194,12 +194,12 @@ impl AssetLoader for () { type Asset = (); type Settings = (); type Error = std::io::Error; - fn load<'a>( + async fn load<'a>( &'a self, - _reader: &'a mut crate::io::Reader, + _reader: &'a mut crate::io::Reader<'_>, _settings: &'a Self::Settings, - _load_context: &'a mut crate::LoadContext, - ) -> bevy_utils::BoxedFuture<'a, Result> { + _load_context: &'a mut crate::LoadContext<'_>, + ) -> Result { unreachable!(); } diff --git a/crates/bevy_asset/src/processor/mod.rs b/crates/bevy_asset/src/processor/mod.rs index ace12c8f7301be..a507ea654716b8 100644 --- a/crates/bevy_asset/src/processor/mod.rs +++ b/crates/bevy_asset/src/processor/mod.rs @@ -6,8 +6,9 @@ pub use process::*; use crate::{ io::{ - AssetReader, AssetReaderError, AssetSource, AssetSourceBuilders, AssetSourceEvent, - AssetSourceId, AssetSources, AssetWriter, AssetWriterError, MissingAssetSourceError, + AssetReaderError, AssetSource, AssetSourceBuilders, AssetSourceEvent, AssetSourceId, + AssetSources, AssetWriterError, ErasedAssetReader, ErasedAssetWriter, + MissingAssetSourceError, }, meta::{ get_asset_hash, get_full_asset_hash, AssetAction, AssetActionMinimal, AssetHash, AssetMeta, @@ -30,6 +31,10 @@ use std::{ }; use thiserror::Error; +// Needed for doc strings +#[allow(unused_imports)] +use crate::io::{AssetReader, AssetWriter}; + /// A "background" asset processor that reads asset values from a source [`AssetSource`] (which corresponds to an [`AssetReader`] / [`AssetWriter`] pair), /// processes them in some way, and writes them to a destination [`AssetSource`]. /// @@ -510,8 +515,8 @@ impl AssetProcessor { /// Retrieves asset paths recursively. If `clean_empty_folders_writer` is Some, it will be used to clean up empty /// folders when they are discovered. fn get_asset_paths<'a>( - reader: &'a dyn AssetReader, - clean_empty_folders_writer: Option<&'a dyn AssetWriter>, + reader: &'a dyn ErasedAssetReader, + clean_empty_folders_writer: Option<&'a dyn ErasedAssetWriter>, path: PathBuf, paths: &'a mut Vec, ) -> BoxedFuture<'a, Result> { diff --git a/crates/bevy_asset/src/processor/process.rs b/crates/bevy_asset/src/processor/process.rs index 75b10acfa26408..b2fca01c123d4d 100644 --- a/crates/bevy_asset/src/processor/process.rs +++ b/crates/bevy_asset/src/processor/process.rs @@ -10,7 +10,7 @@ use crate::{ AssetLoadError, AssetLoader, AssetPath, DeserializeMetaError, ErasedLoadedAsset, MissingAssetLoaderForExtensionError, MissingAssetLoaderForTypeNameError, }; -use bevy_utils::BoxedFuture; +use bevy_utils::{BoxedFuture, ConditionalSendFuture}; use serde::{Deserialize, Serialize}; use std::marker::PhantomData; use thiserror::Error; @@ -32,7 +32,9 @@ pub trait Process: Send + Sync + Sized + 'static { context: &'a mut ProcessContext, meta: AssetMeta<(), Self>, writer: &'a mut Writer, - ) -> BoxedFuture<'a, Result<::Settings, ProcessError>>; + ) -> impl ConditionalSendFuture< + Output = Result<::Settings, ProcessError>, + >; } /// A flexible [`Process`] implementation that loads the source [`Asset`] using the `L` [`AssetLoader`], then transforms @@ -173,41 +175,38 @@ impl< type Settings = LoadTransformAndSaveSettings; type OutputLoader = Saver::OutputLoader; - fn process<'a>( + async fn process<'a>( &'a self, - context: &'a mut ProcessContext, + context: &'a mut ProcessContext<'_>, meta: AssetMeta<(), Self>, writer: &'a mut Writer, - ) -> BoxedFuture<'a, Result<::Settings, ProcessError>> { - Box::pin(async move { - let AssetAction::Process { settings, .. } = meta.asset else { - return Err(ProcessError::WrongMetaType); - }; - let loader_meta = AssetMeta::::new(AssetAction::Load { - loader: std::any::type_name::().to_string(), - settings: settings.loader_settings, - }); - let pre_transformed_asset = TransformedAsset::::from_loaded( - context.load_source_asset(loader_meta).await?, - ) - .unwrap(); + ) -> Result<::Settings, ProcessError> { + let AssetAction::Process { settings, .. } = meta.asset else { + return Err(ProcessError::WrongMetaType); + }; + let loader_meta = AssetMeta::::new(AssetAction::Load { + loader: std::any::type_name::().to_string(), + settings: settings.loader_settings, + }); + let pre_transformed_asset = TransformedAsset::::from_loaded( + context.load_source_asset(loader_meta).await?, + ) + .unwrap(); - let post_transformed_asset = self - .transformer - .transform(pre_transformed_asset, &settings.transformer_settings) - .await - .map_err(|err| ProcessError::AssetTransformError(err.into()))?; + let post_transformed_asset = self + .transformer + .transform(pre_transformed_asset, &settings.transformer_settings) + .await + .map_err(|err| ProcessError::AssetTransformError(err.into()))?; - let saved_asset = - SavedAsset::::from_transformed(&post_transformed_asset); + let saved_asset = SavedAsset::::from_transformed(&post_transformed_asset); - let output_settings = self - .saver - .save(writer, saved_asset, &settings.saver_settings) - .await - .map_err(|error| ProcessError::AssetSaveError(error.into()))?; - Ok(output_settings) - }) + let output_settings = self + .saver + .save(writer, saved_asset, &settings.saver_settings) + .await + .map_err(|error| ProcessError::AssetSaveError(error.into()))?; + Ok(output_settings) } } @@ -217,29 +216,27 @@ impl> Process type Settings = LoadAndSaveSettings; type OutputLoader = Saver::OutputLoader; - fn process<'a>( + async fn process<'a>( &'a self, - context: &'a mut ProcessContext, + context: &'a mut ProcessContext<'_>, meta: AssetMeta<(), Self>, writer: &'a mut Writer, - ) -> BoxedFuture<'a, Result<::Settings, ProcessError>> { - Box::pin(async move { - let AssetAction::Process { settings, .. } = meta.asset else { - return Err(ProcessError::WrongMetaType); - }; - let loader_meta = AssetMeta::::new(AssetAction::Load { - loader: std::any::type_name::().to_string(), - settings: settings.loader_settings, - }); - let loaded_asset = context.load_source_asset(loader_meta).await?; - let saved_asset = SavedAsset::::from_loaded(&loaded_asset).unwrap(); - let output_settings = self - .saver - .save(writer, saved_asset, &settings.saver_settings) - .await - .map_err(|error| ProcessError::AssetSaveError(error.into()))?; - Ok(output_settings) - }) + ) -> Result<::Settings, ProcessError> { + let AssetAction::Process { settings, .. } = meta.asset else { + return Err(ProcessError::WrongMetaType); + }; + let loader_meta = AssetMeta::::new(AssetAction::Load { + loader: std::any::type_name::().to_string(), + settings: settings.loader_settings, + }); + let loaded_asset = context.load_source_asset(loader_meta).await?; + let saved_asset = SavedAsset::::from_loaded(&loaded_asset).unwrap(); + let output_settings = self + .saver + .save(writer, saved_asset, &settings.saver_settings) + .await + .map_err(|error| ProcessError::AssetSaveError(error.into()))?; + Ok(output_settings) } } diff --git a/crates/bevy_asset/src/saver.rs b/crates/bevy_asset/src/saver.rs index a366338f7c3762..36408dd125f298 100644 --- a/crates/bevy_asset/src/saver.rs +++ b/crates/bevy_asset/src/saver.rs @@ -1,7 +1,7 @@ use crate::transformer::TransformedAsset; use crate::{io::Writer, meta::Settings, Asset, ErasedLoadedAsset}; use crate::{AssetLoader, Handle, LabeledAsset, UntypedHandle}; -use bevy_utils::{BoxedFuture, CowArc, HashMap}; +use bevy_utils::{BoxedFuture, ConditionalSendFuture, CowArc, HashMap}; use serde::{Deserialize, Serialize}; use std::{borrow::Borrow, hash::Hash, ops::Deref}; @@ -24,7 +24,9 @@ pub trait AssetSaver: Send + Sync + 'static { writer: &'a mut Writer, asset: SavedAsset<'a, Self::Asset>, settings: &'a Self::Settings, - ) -> BoxedFuture<'a, Result<::Settings, Self::Error>>; + ) -> impl ConditionalSendFuture< + Output = Result<::Settings, Self::Error>, + >; } /// A type-erased dynamic variant of [`AssetSaver`] that allows callers to save assets without knowing the actual type of the [`AssetSaver`]. diff --git a/crates/bevy_asset/src/server/loaders.rs b/crates/bevy_asset/src/server/loaders.rs index 98cc3bce9d28fd..05d2c4873ec373 100644 --- a/crates/bevy_asset/src/server/loaders.rs +++ b/crates/bevy_asset/src/server/loaders.rs @@ -341,21 +341,19 @@ mod tests { type Error = String; - fn load<'a>( + async fn load<'a>( &'a self, - _: &'a mut crate::io::Reader, + _: &'a mut crate::io::Reader<'_>, _: &'a Self::Settings, - _: &'a mut crate::LoadContext, - ) -> bevy_utils::BoxedFuture<'a, Result> { + _: &'a mut crate::LoadContext<'_>, + ) -> Result { self.sender.send(()).unwrap(); - Box::pin(async move { - Err(format!( - "Loaded {}:{}", - std::any::type_name::(), - N - )) - }) + Err(format!( + "Loaded {}:{}", + std::any::type_name::(), + N + )) } fn extensions(&self) -> &[&str] { diff --git a/crates/bevy_asset/src/server/mod.rs b/crates/bevy_asset/src/server/mod.rs index 2be2848aece486..60536cc40509e1 100644 --- a/crates/bevy_asset/src/server/mod.rs +++ b/crates/bevy_asset/src/server/mod.rs @@ -4,8 +4,8 @@ mod loaders; use crate::{ folder::LoadedFolder, io::{ - AssetReader, AssetReaderError, AssetSource, AssetSourceEvent, AssetSourceId, AssetSources, - MissingAssetSourceError, MissingProcessedAssetReaderError, Reader, + AssetReaderError, AssetSource, AssetSourceEvent, AssetSourceId, AssetSources, + ErasedAssetReader, MissingAssetSourceError, MissingProcessedAssetReaderError, Reader, }, loader::{AssetLoader, ErasedAssetLoader, LoadContext, LoadedAsset}, meta::{ @@ -30,6 +30,10 @@ use std::path::PathBuf; use std::{any::TypeId, path::Path, sync::Arc}; use thiserror::Error; +// Needed for doc string +#[allow(unused_imports)] +use crate::io::{AssetReader, AssetWriter}; + /// Loads and tracks the state of [`Asset`] values from a configured [`AssetReader`]. This can be used to kick off new asset loads and /// retrieve their current load states. /// @@ -657,7 +661,7 @@ impl AssetServer { fn load_folder<'a>( source: AssetSourceId<'static>, path: &'a Path, - reader: &'a dyn AssetReader, + reader: &'a dyn ErasedAssetReader, server: &'a AssetServer, handles: &'a mut Vec, ) -> bevy_utils::BoxedFuture<'a, Result<(), AssetLoadError>> { diff --git a/crates/bevy_asset/src/transformer.rs b/crates/bevy_asset/src/transformer.rs index 3b8ae58bc37cd5..0ffddc4658a432 100644 --- a/crates/bevy_asset/src/transformer.rs +++ b/crates/bevy_asset/src/transformer.rs @@ -1,5 +1,5 @@ use crate::{meta::Settings, Asset, ErasedLoadedAsset, Handle, LabeledAsset, UntypedHandle}; -use bevy_utils::{BoxedFuture, CowArc, HashMap}; +use bevy_utils::{ConditionalSendFuture, CowArc, HashMap}; use serde::{Deserialize, Serialize}; use std::{ borrow::Borrow, @@ -25,7 +25,7 @@ pub trait AssetTransformer: Send + Sync + 'static { &'a self, asset: TransformedAsset, settings: &'a Self::Settings, - ) -> BoxedFuture<'a, Result, Self::Error>>; + ) -> impl ConditionalSendFuture, Self::Error>>; } /// An [`Asset`] (and any "sub assets") intended to be transformed diff --git a/crates/bevy_audio/src/audio_source.rs b/crates/bevy_audio/src/audio_source.rs index 8b0c7090eac141..242c6a6a7c6641 100644 --- a/crates/bevy_audio/src/audio_source.rs +++ b/crates/bevy_audio/src/audio_source.rs @@ -3,7 +3,6 @@ use bevy_asset::{ Asset, AssetLoader, LoadContext, }; use bevy_reflect::TypePath; -use bevy_utils::BoxedFuture; use std::{io::Cursor, sync::Arc}; /// A source of audio data @@ -43,18 +42,16 @@ impl AssetLoader for AudioLoader { type Settings = (); type Error = std::io::Error; - fn load<'a>( + async fn load<'a>( &'a self, - reader: &'a mut Reader, + reader: &'a mut Reader<'_>, _settings: &'a Self::Settings, - _load_context: &'a mut LoadContext, - ) -> BoxedFuture<'a, Result> { - Box::pin(async move { - let mut bytes = Vec::new(); - reader.read_to_end(&mut bytes).await?; - Ok(AudioSource { - bytes: bytes.into(), - }) + _load_context: &'a mut LoadContext<'_>, + ) -> Result { + let mut bytes = Vec::new(); + reader.read_to_end(&mut bytes).await?; + Ok(AudioSource { + bytes: bytes.into(), }) } diff --git a/crates/bevy_color/src/laba.rs b/crates/bevy_color/src/laba.rs index 10decc050439ed..a227cc271e5d24 100644 --- a/crates/bevy_color/src/laba.rs +++ b/crates/bevy_color/src/laba.rs @@ -1,6 +1,6 @@ use crate::{ - Alpha, ClampColor, Hsla, Hsva, Hwba, LinearRgba, Luminance, Mix, Oklaba, Srgba, StandardColor, - Xyza, + impl_componentwise_point, Alpha, ClampColor, Hsla, Hsva, Hwba, LinearRgba, Luminance, Mix, + Oklaba, Srgba, StandardColor, Xyza, }; use bevy_reflect::prelude::*; use serde::{Deserialize, Serialize}; @@ -25,6 +25,8 @@ pub struct Laba { impl StandardColor for Laba {} +impl_componentwise_point!(Laba, [lightness, a, b, alpha]); + impl Laba { /// Construct a new [`Laba`] color from components. /// diff --git a/crates/bevy_color/src/lib.rs b/crates/bevy_color/src/lib.rs index 27d92be1c46b24..937921503e7ca9 100644 --- a/crates/bevy_color/src/lib.rs +++ b/crates/bevy_color/src/lib.rs @@ -65,6 +65,12 @@ //! types in this crate. This is useful when you need to store a color in a data structure //! that can't be generic over the color type. //! +//! Color types that are either physically or perceptually linear also implement `Add`, `Sub`, `Mul` and `Div` +//! allowing you to use them with splines. +//! +//! Please note that most often adding or subtracting colors is not what you may want. +//! Please have a look at other operations like blending, lightening or mixing colors using e.g. [`Mix`] or [`Luminance`] instead. +//! //! # Example //! //! ``` @@ -151,3 +157,61 @@ where Self: Alpha, { } + +macro_rules! impl_componentwise_point { + ($ty: ident, [$($element: ident),+]) => { + impl std::ops::Add for $ty { + type Output = Self; + + fn add(self, rhs: Self) -> Self::Output { + Self::Output { + $($element: self.$element + rhs.$element,)+ + } + } + } + + impl std::ops::Sub for $ty { + type Output = Self; + + fn sub(self, rhs: Self) -> Self::Output { + Self::Output { + $($element: self.$element - rhs.$element,)+ + } + } + } + + impl std::ops::Mul for $ty { + type Output = Self; + + fn mul(self, rhs: f32) -> Self::Output { + Self::Output { + $($element: self.$element * rhs,)+ + } + } + } + + impl std::ops::Mul<$ty> for f32 { + type Output = $ty; + + fn mul(self, rhs: $ty) -> Self::Output { + Self::Output { + $($element: self * rhs.$element,)+ + } + } + } + + impl std::ops::Div for $ty { + type Output = Self; + + fn div(self, rhs: f32) -> Self::Output { + Self::Output { + $($element: self.$element / rhs,)+ + } + } + } + + impl bevy_math::cubic_splines::Point for $ty {} + }; +} + +pub(crate) use impl_componentwise_point; diff --git a/crates/bevy_color/src/linear_rgba.rs b/crates/bevy_color/src/linear_rgba.rs index 864b402a28fe2f..5e450af72f31cf 100644 --- a/crates/bevy_color/src/linear_rgba.rs +++ b/crates/bevy_color/src/linear_rgba.rs @@ -1,7 +1,6 @@ -use std::ops::{Div, Mul}; - use crate::{ - color_difference::EuclideanDistance, Alpha, ClampColor, Luminance, Mix, StandardColor, + color_difference::EuclideanDistance, impl_componentwise_point, Alpha, ClampColor, Luminance, + Mix, StandardColor, }; use bevy_math::Vec4; use bevy_reflect::prelude::*; @@ -29,6 +28,8 @@ pub struct LinearRgba { impl StandardColor for LinearRgba {} +impl_componentwise_point!(LinearRgba, [red, green, blue, alpha]); + impl LinearRgba { /// A fully black color with full alpha. pub const BLACK: Self = Self { @@ -299,48 +300,6 @@ impl From for wgpu::Color { } } -/// All color channels are scaled directly, -/// but alpha is unchanged. -/// -/// Values are not clamped. -impl Mul for LinearRgba { - type Output = Self; - - fn mul(self, rhs: f32) -> Self { - Self { - red: self.red * rhs, - green: self.green * rhs, - blue: self.blue * rhs, - alpha: self.alpha, - } - } -} - -impl Mul for f32 { - type Output = LinearRgba; - - fn mul(self, rhs: LinearRgba) -> LinearRgba { - rhs * self - } -} - -/// All color channels are scaled directly, -/// but alpha is unchanged. -/// -/// Values are not clamped. -impl Div for LinearRgba { - type Output = Self; - - fn div(self, rhs: f32) -> Self { - Self { - red: self.red / rhs, - green: self.green / rhs, - blue: self.blue / rhs, - alpha: self.alpha, - } - } -} - // [`LinearRgba`] is intended to be used with shaders // So it's the only color type that implements [`ShaderType`] to make it easier to use inside shaders impl encase::ShaderType for LinearRgba { diff --git a/crates/bevy_color/src/oklaba.rs b/crates/bevy_color/src/oklaba.rs index 2ec202a2fa22b4..5501e8e4c6ec8b 100644 --- a/crates/bevy_color/src/oklaba.rs +++ b/crates/bevy_color/src/oklaba.rs @@ -1,6 +1,6 @@ use crate::{ - color_difference::EuclideanDistance, Alpha, ClampColor, Hsla, Hsva, Hwba, Lcha, LinearRgba, - Luminance, Mix, Srgba, StandardColor, Xyza, + color_difference::EuclideanDistance, impl_componentwise_point, Alpha, ClampColor, Hsla, Hsva, + Hwba, Lcha, LinearRgba, Luminance, Mix, Srgba, StandardColor, Xyza, }; use bevy_reflect::prelude::*; use serde::{Deserialize, Serialize}; @@ -13,7 +13,7 @@ use serde::{Deserialize, Serialize}; #[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize, Reflect)] #[reflect(PartialEq, Serialize, Deserialize, Default)] pub struct Oklaba { - /// The 'l' channel. [0.0, 1.0] + /// The 'lightness' channel. [0.0, 1.0] pub lightness: f32, /// The 'a' channel. [-1.0, 1.0] pub a: f32, @@ -25,6 +25,8 @@ pub struct Oklaba { impl StandardColor for Oklaba {} +impl_componentwise_point!(Oklaba, [lightness, a, b, alpha]); + impl Oklaba { /// Construct a new [`Oklaba`] color from components. /// @@ -59,7 +61,7 @@ impl Oklaba { } } - /// Return a copy of this color with the 'l' channel set to the given value. + /// Return a copy of this color with the 'lightness' channel set to the given value. pub const fn with_lightness(self, lightness: f32) -> Self { Self { lightness, ..self } } diff --git a/crates/bevy_color/src/oklcha.rs b/crates/bevy_color/src/oklcha.rs index 4185990e5d4165..133794e256b35b 100644 --- a/crates/bevy_color/src/oklcha.rs +++ b/crates/bevy_color/src/oklcha.rs @@ -56,17 +56,17 @@ impl Oklcha { } /// Return a copy of this color with the 'lightness' channel set to the given value. - pub const fn with_l(self, lightness: f32) -> Self { + pub const fn with_lightness(self, lightness: f32) -> Self { Self { lightness, ..self } } /// Return a copy of this color with the 'chroma' channel set to the given value. - pub const fn with_c(self, chroma: f32) -> Self { + pub const fn with_chroma(self, chroma: f32) -> Self { Self { chroma, ..self } } /// Return a copy of this color with the 'hue' channel set to the given value. - pub const fn with_h(self, hue: f32) -> Self { + pub const fn with_hue(self, hue: f32) -> Self { Self { hue, ..self } } diff --git a/crates/bevy_color/src/palettes/mod.rs b/crates/bevy_color/src/palettes/mod.rs index f062ebedbabc16..d050ea685ae9a7 100644 --- a/crates/bevy_color/src/palettes/mod.rs +++ b/crates/bevy_color/src/palettes/mod.rs @@ -2,3 +2,4 @@ pub mod basic; pub mod css; +pub mod tailwind; diff --git a/crates/bevy_color/src/palettes/tailwind.rs b/crates/bevy_color/src/palettes/tailwind.rs new file mode 100644 index 00000000000000..31eb9d42e06e35 --- /dev/null +++ b/crates/bevy_color/src/palettes/tailwind.rs @@ -0,0 +1,536 @@ +//! Colors from [Tailwind CSS](https://tailwindcss.com/docs/customizing-colors) (MIT License). +//! Grouped by hue with numeric lightness scale (50 is light, 950 is dark). +//! +//! Generated from Tailwind 3.4.1. + +/* +MIT License + +Copyright (c) Tailwind Labs, Inc. + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +*/ + +use crate::Srgba; + +///
+pub const AMBER_50: Srgba = Srgba::rgb(1.0, 0.9843137, 0.92156863); +///
+pub const AMBER_100: Srgba = Srgba::rgb(0.99607843, 0.9529412, 0.78039217); +///
+pub const AMBER_200: Srgba = Srgba::rgb(0.99215686, 0.9019608, 0.5411765); +///
+pub const AMBER_300: Srgba = Srgba::rgb(0.9882353, 0.827451, 0.3019608); +///
+pub const AMBER_400: Srgba = Srgba::rgb(0.9843137, 0.7490196, 0.14117648); +///
+pub const AMBER_500: Srgba = Srgba::rgb(0.9607843, 0.61960787, 0.043137256); +///
+pub const AMBER_600: Srgba = Srgba::rgb(0.8509804, 0.46666667, 0.023529412); +///
+pub const AMBER_700: Srgba = Srgba::rgb(0.7058824, 0.3254902, 0.03529412); +///
+pub const AMBER_800: Srgba = Srgba::rgb(0.57254905, 0.2509804, 0.05490196); +///
+pub const AMBER_900: Srgba = Srgba::rgb(0.47058824, 0.20784314, 0.05882353); +///
+pub const AMBER_950: Srgba = Srgba::rgb(0.27058825, 0.101960786, 0.011764706); + +///
+pub const BLUE_50: Srgba = Srgba::rgb(0.9372549, 0.9647059, 1.0); +///
+pub const BLUE_100: Srgba = Srgba::rgb(0.85882354, 0.91764706, 0.99607843); +///
+pub const BLUE_200: Srgba = Srgba::rgb(0.7490196, 0.85882354, 0.99607843); +///
+pub const BLUE_300: Srgba = Srgba::rgb(0.5764706, 0.77254903, 0.99215686); +///
+pub const BLUE_400: Srgba = Srgba::rgb(0.3764706, 0.64705884, 0.98039216); +///
+pub const BLUE_500: Srgba = Srgba::rgb(0.23137255, 0.50980395, 0.9647059); +///
+pub const BLUE_600: Srgba = Srgba::rgb(0.14509805, 0.3882353, 0.92156863); +///
+pub const BLUE_700: Srgba = Srgba::rgb(0.11372549, 0.30588236, 0.84705883); +///
+pub const BLUE_800: Srgba = Srgba::rgb(0.11764706, 0.2509804, 0.6862745); +///
+pub const BLUE_900: Srgba = Srgba::rgb(0.11764706, 0.22745098, 0.5411765); +///
+pub const BLUE_950: Srgba = Srgba::rgb(0.09019608, 0.14509805, 0.32941177); + +///
+pub const CYAN_50: Srgba = Srgba::rgb(0.9254902, 0.99607843, 1.0); +///
+pub const CYAN_100: Srgba = Srgba::rgb(0.8117647, 0.98039216, 0.99607843); +///
+pub const CYAN_200: Srgba = Srgba::rgb(0.64705884, 0.9529412, 0.9882353); +///
+pub const CYAN_300: Srgba = Srgba::rgb(0.40392157, 0.9098039, 0.9764706); +///
+pub const CYAN_400: Srgba = Srgba::rgb(0.13333334, 0.827451, 0.93333334); +///
+pub const CYAN_500: Srgba = Srgba::rgb(0.023529412, 0.7137255, 0.83137256); +///
+pub const CYAN_600: Srgba = Srgba::rgb(0.03137255, 0.5686275, 0.69803923); +///
+pub const CYAN_700: Srgba = Srgba::rgb(0.05490196, 0.45490196, 0.5647059); +///
+pub const CYAN_800: Srgba = Srgba::rgb(0.08235294, 0.36862746, 0.45882353); +///
+pub const CYAN_900: Srgba = Srgba::rgb(0.08627451, 0.30588236, 0.3882353); +///
+pub const CYAN_950: Srgba = Srgba::rgb(0.03137255, 0.2, 0.26666668); + +///
+pub const EMERALD_50: Srgba = Srgba::rgb(0.9254902, 0.99215686, 0.9607843); +///
+pub const EMERALD_100: Srgba = Srgba::rgb(0.81960785, 0.98039216, 0.8980392); +///
+pub const EMERALD_200: Srgba = Srgba::rgb(0.654902, 0.9529412, 0.8156863); +///
+pub const EMERALD_300: Srgba = Srgba::rgb(0.43137255, 0.90588236, 0.7176471); +///
+pub const EMERALD_400: Srgba = Srgba::rgb(0.20392157, 0.827451, 0.6); +///
+pub const EMERALD_500: Srgba = Srgba::rgb(0.0627451, 0.7254902, 0.5058824); +///
+pub const EMERALD_600: Srgba = Srgba::rgb(0.019607844, 0.5882353, 0.4117647); +///
+pub const EMERALD_700: Srgba = Srgba::rgb(0.015686275, 0.47058824, 0.34117648); +///
+pub const EMERALD_800: Srgba = Srgba::rgb(0.023529412, 0.37254903, 0.27450982); +///
+pub const EMERALD_900: Srgba = Srgba::rgb(0.023529412, 0.30588236, 0.23137255); +///
+pub const EMERALD_950: Srgba = Srgba::rgb(0.007843138, 0.17254902, 0.13333334); + +///
+pub const FUCHSIA_50: Srgba = Srgba::rgb(0.99215686, 0.95686275, 1.0); +///
+pub const FUCHSIA_100: Srgba = Srgba::rgb(0.98039216, 0.9098039, 1.0); +///
+pub const FUCHSIA_200: Srgba = Srgba::rgb(0.9607843, 0.8156863, 0.99607843); +///
+pub const FUCHSIA_300: Srgba = Srgba::rgb(0.9411765, 0.67058825, 0.9882353); +///
+pub const FUCHSIA_400: Srgba = Srgba::rgb(0.9098039, 0.4745098, 0.9764706); +///
+pub const FUCHSIA_500: Srgba = Srgba::rgb(0.8509804, 0.27450982, 0.9372549); +///
+pub const FUCHSIA_600: Srgba = Srgba::rgb(0.7529412, 0.14901961, 0.827451); +///
+pub const FUCHSIA_700: Srgba = Srgba::rgb(0.63529414, 0.10980392, 0.6862745); +///
+pub const FUCHSIA_800: Srgba = Srgba::rgb(0.5254902, 0.09803922, 0.56078434); +///
+pub const FUCHSIA_900: Srgba = Srgba::rgb(0.4392157, 0.101960786, 0.45882353); +///
+pub const FUCHSIA_950: Srgba = Srgba::rgb(0.2901961, 0.015686275, 0.30588236); + +///
+pub const GRAY_50: Srgba = Srgba::rgb(0.9764706, 0.98039216, 0.9843137); +///
+pub const GRAY_100: Srgba = Srgba::rgb(0.9529412, 0.95686275, 0.9647059); +///
+pub const GRAY_200: Srgba = Srgba::rgb(0.8980392, 0.90588236, 0.92156863); +///
+pub const GRAY_300: Srgba = Srgba::rgb(0.81960785, 0.8352941, 0.85882354); +///
+pub const GRAY_400: Srgba = Srgba::rgb(0.6117647, 0.6392157, 0.6862745); +///
+pub const GRAY_500: Srgba = Srgba::rgb(0.41960785, 0.44705883, 0.5019608); +///
+pub const GRAY_600: Srgba = Srgba::rgb(0.29411766, 0.33333334, 0.3882353); +///
+pub const GRAY_700: Srgba = Srgba::rgb(0.21568628, 0.25490198, 0.31764707); +///
+pub const GRAY_800: Srgba = Srgba::rgb(0.12156863, 0.16078432, 0.21568628); +///
+pub const GRAY_900: Srgba = Srgba::rgb(0.06666667, 0.09411765, 0.15294118); +///
+pub const GRAY_950: Srgba = Srgba::rgb(0.011764706, 0.02745098, 0.07058824); + +///
+pub const GREEN_50: Srgba = Srgba::rgb(0.9411765, 0.99215686, 0.95686275); +///
+pub const GREEN_100: Srgba = Srgba::rgb(0.8627451, 0.9882353, 0.90588236); +///
+pub const GREEN_200: Srgba = Srgba::rgb(0.73333335, 0.96862745, 0.8156863); +///
+pub const GREEN_300: Srgba = Srgba::rgb(0.5254902, 0.9372549, 0.6745098); +///
+pub const GREEN_400: Srgba = Srgba::rgb(0.2901961, 0.87058824, 0.5019608); +///
+pub const GREEN_500: Srgba = Srgba::rgb(0.13333334, 0.77254903, 0.36862746); +///
+pub const GREEN_600: Srgba = Srgba::rgb(0.08627451, 0.6392157, 0.2901961); +///
+pub const GREEN_700: Srgba = Srgba::rgb(0.08235294, 0.5019608, 0.23921569); +///
+pub const GREEN_800: Srgba = Srgba::rgb(0.08627451, 0.39607844, 0.20392157); +///
+pub const GREEN_900: Srgba = Srgba::rgb(0.078431375, 0.3254902, 0.1764706); +///
+pub const GREEN_950: Srgba = Srgba::rgb(0.019607844, 0.18039216, 0.08627451); + +///
+pub const INDIGO_50: Srgba = Srgba::rgb(0.93333334, 0.9490196, 1.0); +///
+pub const INDIGO_100: Srgba = Srgba::rgb(0.8784314, 0.90588236, 1.0); +///
+pub const INDIGO_200: Srgba = Srgba::rgb(0.78039217, 0.8235294, 0.99607843); +///
+pub const INDIGO_300: Srgba = Srgba::rgb(0.64705884, 0.7058824, 0.9882353); +///
+pub const INDIGO_400: Srgba = Srgba::rgb(0.5058824, 0.54901963, 0.972549); +///
+pub const INDIGO_500: Srgba = Srgba::rgb(0.3882353, 0.4, 0.94509804); +///
+pub const INDIGO_600: Srgba = Srgba::rgb(0.30980393, 0.27450982, 0.8980392); +///
+pub const INDIGO_700: Srgba = Srgba::rgb(0.2627451, 0.21960784, 0.7921569); +///
+pub const INDIGO_800: Srgba = Srgba::rgb(0.21568628, 0.1882353, 0.6392157); +///
+pub const INDIGO_900: Srgba = Srgba::rgb(0.19215687, 0.18039216, 0.5058824); +///
+pub const INDIGO_950: Srgba = Srgba::rgb(0.11764706, 0.105882354, 0.29411766); + +///
+pub const LIME_50: Srgba = Srgba::rgb(0.96862745, 0.99607843, 0.90588236); +///
+pub const LIME_100: Srgba = Srgba::rgb(0.9254902, 0.9882353, 0.79607844); +///
+pub const LIME_200: Srgba = Srgba::rgb(0.8509804, 0.9764706, 0.6156863); +///
+pub const LIME_300: Srgba = Srgba::rgb(0.74509805, 0.9490196, 0.39215687); +///
+pub const LIME_400: Srgba = Srgba::rgb(0.6392157, 0.9019608, 0.20784314); +///
+pub const LIME_500: Srgba = Srgba::rgb(0.5176471, 0.8, 0.08627451); +///
+pub const LIME_600: Srgba = Srgba::rgb(0.39607844, 0.6392157, 0.050980393); +///
+pub const LIME_700: Srgba = Srgba::rgb(0.3019608, 0.4862745, 0.05882353); +///
+pub const LIME_800: Srgba = Srgba::rgb(0.24705882, 0.38431373, 0.07058824); +///
+pub const LIME_900: Srgba = Srgba::rgb(0.21176471, 0.3254902, 0.078431375); +///
+pub const LIME_950: Srgba = Srgba::rgb(0.101960786, 0.18039216, 0.019607844); + +///
+pub const NEUTRAL_50: Srgba = Srgba::rgb(0.98039216, 0.98039216, 0.98039216); +///
+pub const NEUTRAL_100: Srgba = Srgba::rgb(0.9607843, 0.9607843, 0.9607843); +///
+pub const NEUTRAL_200: Srgba = Srgba::rgb(0.8980392, 0.8980392, 0.8980392); +///
+pub const NEUTRAL_300: Srgba = Srgba::rgb(0.83137256, 0.83137256, 0.83137256); +///
+pub const NEUTRAL_400: Srgba = Srgba::rgb(0.6392157, 0.6392157, 0.6392157); +///
+pub const NEUTRAL_500: Srgba = Srgba::rgb(0.4509804, 0.4509804, 0.4509804); +///
+pub const NEUTRAL_600: Srgba = Srgba::rgb(0.32156864, 0.32156864, 0.32156864); +///
+pub const NEUTRAL_700: Srgba = Srgba::rgb(0.2509804, 0.2509804, 0.2509804); +///
+pub const NEUTRAL_800: Srgba = Srgba::rgb(0.14901961, 0.14901961, 0.14901961); +///
+pub const NEUTRAL_900: Srgba = Srgba::rgb(0.09019608, 0.09019608, 0.09019608); +///
+pub const NEUTRAL_950: Srgba = Srgba::rgb(0.039215688, 0.039215688, 0.039215688); + +///
+pub const ORANGE_50: Srgba = Srgba::rgb(1.0, 0.96862745, 0.92941177); +///
+pub const ORANGE_100: Srgba = Srgba::rgb(1.0, 0.92941177, 0.8352941); +///
+pub const ORANGE_200: Srgba = Srgba::rgb(0.99607843, 0.84313726, 0.6666667); +///
+pub const ORANGE_300: Srgba = Srgba::rgb(0.99215686, 0.7294118, 0.45490196); +///
+pub const ORANGE_400: Srgba = Srgba::rgb(0.9843137, 0.57254905, 0.23529412); +///
+pub const ORANGE_500: Srgba = Srgba::rgb(0.9764706, 0.4509804, 0.08627451); +///
+pub const ORANGE_600: Srgba = Srgba::rgb(0.91764706, 0.34509805, 0.047058824); +///
+pub const ORANGE_700: Srgba = Srgba::rgb(0.7607843, 0.25490198, 0.047058824); +///
+pub const ORANGE_800: Srgba = Srgba::rgb(0.6039216, 0.20392157, 0.07058824); +///
+pub const ORANGE_900: Srgba = Srgba::rgb(0.4862745, 0.1764706, 0.07058824); +///
+pub const ORANGE_950: Srgba = Srgba::rgb(0.2627451, 0.078431375, 0.02745098); + +///
+pub const PINK_50: Srgba = Srgba::rgb(0.99215686, 0.9490196, 0.972549); +///
+pub const PINK_100: Srgba = Srgba::rgb(0.9882353, 0.90588236, 0.9529412); +///
+pub const PINK_200: Srgba = Srgba::rgb(0.9843137, 0.8117647, 0.9098039); +///
+pub const PINK_300: Srgba = Srgba::rgb(0.9764706, 0.65882355, 0.83137256); +///
+pub const PINK_400: Srgba = Srgba::rgb(0.95686275, 0.44705883, 0.7137255); +///
+pub const PINK_500: Srgba = Srgba::rgb(0.9254902, 0.28235295, 0.6); +///
+pub const PINK_600: Srgba = Srgba::rgb(0.85882354, 0.15294118, 0.46666667); +///
+pub const PINK_700: Srgba = Srgba::rgb(0.74509805, 0.09411765, 0.3647059); +///
+pub const PINK_800: Srgba = Srgba::rgb(0.6156863, 0.09019608, 0.3019608); +///
+pub const PINK_900: Srgba = Srgba::rgb(0.5137255, 0.09411765, 0.2627451); +///
+pub const PINK_950: Srgba = Srgba::rgb(0.3137255, 0.02745098, 0.14117648); + +///
+pub const PURPLE_50: Srgba = Srgba::rgb(0.98039216, 0.9607843, 1.0); +///
+pub const PURPLE_100: Srgba = Srgba::rgb(0.9529412, 0.9098039, 1.0); +///
+pub const PURPLE_200: Srgba = Srgba::rgb(0.9137255, 0.8352941, 1.0); +///
+pub const PURPLE_300: Srgba = Srgba::rgb(0.84705883, 0.7058824, 0.99607843); +///
+pub const PURPLE_400: Srgba = Srgba::rgb(0.7529412, 0.5176471, 0.9882353); +///
+pub const PURPLE_500: Srgba = Srgba::rgb(0.65882355, 0.33333334, 0.96862745); +///
+pub const PURPLE_600: Srgba = Srgba::rgb(0.5764706, 0.2, 0.91764706); +///
+pub const PURPLE_700: Srgba = Srgba::rgb(0.49411765, 0.13333334, 0.80784315); +///
+pub const PURPLE_800: Srgba = Srgba::rgb(0.41960785, 0.12941177, 0.65882355); +///
+pub const PURPLE_900: Srgba = Srgba::rgb(0.34509805, 0.10980392, 0.5294118); +///
+pub const PURPLE_950: Srgba = Srgba::rgb(0.23137255, 0.02745098, 0.39215687); + +///
+pub const RED_50: Srgba = Srgba::rgb(0.99607843, 0.9490196, 0.9490196); +///
+pub const RED_100: Srgba = Srgba::rgb(0.99607843, 0.8862745, 0.8862745); +///
+pub const RED_200: Srgba = Srgba::rgb(0.99607843, 0.7921569, 0.7921569); +///
+pub const RED_300: Srgba = Srgba::rgb(0.9882353, 0.64705884, 0.64705884); +///
+pub const RED_400: Srgba = Srgba::rgb(0.972549, 0.44313726, 0.44313726); +///
+pub const RED_500: Srgba = Srgba::rgb(0.9372549, 0.26666668, 0.26666668); +///
+pub const RED_600: Srgba = Srgba::rgb(0.8627451, 0.14901961, 0.14901961); +///
+pub const RED_700: Srgba = Srgba::rgb(0.7254902, 0.10980392, 0.10980392); +///
+pub const RED_800: Srgba = Srgba::rgb(0.6, 0.105882354, 0.105882354); +///
+pub const RED_900: Srgba = Srgba::rgb(0.49803922, 0.11372549, 0.11372549); +///
+pub const RED_950: Srgba = Srgba::rgb(0.27058825, 0.039215688, 0.039215688); + +///
+pub const ROSE_50: Srgba = Srgba::rgb(1.0, 0.94509804, 0.9490196); +///
+pub const ROSE_100: Srgba = Srgba::rgb(1.0, 0.89411765, 0.9019608); +///
+pub const ROSE_200: Srgba = Srgba::rgb(0.99607843, 0.8039216, 0.827451); +///
+pub const ROSE_300: Srgba = Srgba::rgb(0.99215686, 0.6431373, 0.6862745); +///
+pub const ROSE_400: Srgba = Srgba::rgb(0.9843137, 0.44313726, 0.52156866); +///
+pub const ROSE_500: Srgba = Srgba::rgb(0.95686275, 0.24705882, 0.36862746); +///
+pub const ROSE_600: Srgba = Srgba::rgb(0.88235295, 0.11372549, 0.28235295); +///
+pub const ROSE_700: Srgba = Srgba::rgb(0.74509805, 0.07058824, 0.23529412); +///
+pub const ROSE_800: Srgba = Srgba::rgb(0.62352943, 0.07058824, 0.22352941); +///
+pub const ROSE_900: Srgba = Srgba::rgb(0.53333336, 0.07450981, 0.21568628); +///
+pub const ROSE_950: Srgba = Srgba::rgb(0.29803923, 0.019607844, 0.09803922); + +///
+pub const SKY_50: Srgba = Srgba::rgb(0.9411765, 0.9764706, 1.0); +///
+pub const SKY_100: Srgba = Srgba::rgb(0.8784314, 0.9490196, 0.99607843); +///
+pub const SKY_200: Srgba = Srgba::rgb(0.7294118, 0.9019608, 0.99215686); +///
+pub const SKY_300: Srgba = Srgba::rgb(0.49019608, 0.827451, 0.9882353); +///
+pub const SKY_400: Srgba = Srgba::rgb(0.21960784, 0.7411765, 0.972549); +///
+pub const SKY_500: Srgba = Srgba::rgb(0.05490196, 0.64705884, 0.9137255); +///
+pub const SKY_600: Srgba = Srgba::rgb(0.007843138, 0.5176471, 0.78039217); +///
+pub const SKY_700: Srgba = Srgba::rgb(0.011764706, 0.4117647, 0.6313726); +///
+pub const SKY_800: Srgba = Srgba::rgb(0.02745098, 0.34901962, 0.52156866); +///
+pub const SKY_900: Srgba = Srgba::rgb(0.047058824, 0.2901961, 0.43137255); +///
+pub const SKY_950: Srgba = Srgba::rgb(0.03137255, 0.18431373, 0.28627452); + +///
+pub const SLATE_50: Srgba = Srgba::rgb(0.972549, 0.98039216, 0.9882353); +///
+pub const SLATE_100: Srgba = Srgba::rgb(0.94509804, 0.9607843, 0.9764706); +///
+pub const SLATE_200: Srgba = Srgba::rgb(0.8862745, 0.9098039, 0.9411765); +///
+pub const SLATE_300: Srgba = Srgba::rgb(0.79607844, 0.8352941, 0.88235295); +///
+pub const SLATE_400: Srgba = Srgba::rgb(0.5803922, 0.6392157, 0.72156864); +///
+pub const SLATE_500: Srgba = Srgba::rgb(0.39215687, 0.45490196, 0.54509807); +///
+pub const SLATE_600: Srgba = Srgba::rgb(0.2784314, 0.33333334, 0.4117647); +///
+pub const SLATE_700: Srgba = Srgba::rgb(0.2, 0.25490198, 0.33333334); +///
+pub const SLATE_800: Srgba = Srgba::rgb(0.11764706, 0.16078432, 0.23137255); +///
+pub const SLATE_900: Srgba = Srgba::rgb(0.05882353, 0.09019608, 0.16470589); +///
+pub const SLATE_950: Srgba = Srgba::rgb(0.007843138, 0.023529412, 0.09019608); + +///
+pub const STONE_50: Srgba = Srgba::rgb(0.98039216, 0.98039216, 0.9764706); +///
+pub const STONE_100: Srgba = Srgba::rgb(0.9607843, 0.9607843, 0.95686275); +///
+pub const STONE_200: Srgba = Srgba::rgb(0.90588236, 0.8980392, 0.89411765); +///
+pub const STONE_300: Srgba = Srgba::rgb(0.8392157, 0.827451, 0.81960785); +///
+pub const STONE_400: Srgba = Srgba::rgb(0.65882355, 0.63529414, 0.61960787); +///
+pub const STONE_500: Srgba = Srgba::rgb(0.47058824, 0.44313726, 0.42352942); +///
+pub const STONE_600: Srgba = Srgba::rgb(0.34117648, 0.3254902, 0.30588236); +///
+pub const STONE_700: Srgba = Srgba::rgb(0.26666668, 0.2509804, 0.23529412); +///
+pub const STONE_800: Srgba = Srgba::rgb(0.16078432, 0.14509805, 0.14117648); +///
+pub const STONE_900: Srgba = Srgba::rgb(0.10980392, 0.09803922, 0.09019608); +///
+pub const STONE_950: Srgba = Srgba::rgb(0.047058824, 0.039215688, 0.03529412); + +///
+pub const TEAL_50: Srgba = Srgba::rgb(0.9411765, 0.99215686, 0.98039216); +///
+pub const TEAL_100: Srgba = Srgba::rgb(0.8, 0.9843137, 0.94509804); +///
+pub const TEAL_200: Srgba = Srgba::rgb(0.6, 0.9647059, 0.89411765); +///
+pub const TEAL_300: Srgba = Srgba::rgb(0.36862746, 0.91764706, 0.83137256); +///
+pub const TEAL_400: Srgba = Srgba::rgb(0.1764706, 0.83137256, 0.7490196); +///
+pub const TEAL_500: Srgba = Srgba::rgb(0.078431375, 0.72156864, 0.6509804); +///
+pub const TEAL_600: Srgba = Srgba::rgb(0.050980393, 0.5803922, 0.53333336); +///
+pub const TEAL_700: Srgba = Srgba::rgb(0.05882353, 0.4627451, 0.43137255); +///
+pub const TEAL_800: Srgba = Srgba::rgb(0.06666667, 0.36862746, 0.34901962); +///
+pub const TEAL_900: Srgba = Srgba::rgb(0.07450981, 0.30588236, 0.2901961); +///
+pub const TEAL_950: Srgba = Srgba::rgb(0.015686275, 0.18431373, 0.18039216); + +///
+pub const VIOLET_50: Srgba = Srgba::rgb(0.9607843, 0.9529412, 1.0); +///
+pub const VIOLET_100: Srgba = Srgba::rgb(0.92941177, 0.9137255, 0.99607843); +///
+pub const VIOLET_200: Srgba = Srgba::rgb(0.8666667, 0.8392157, 0.99607843); +///
+pub const VIOLET_300: Srgba = Srgba::rgb(0.76862746, 0.70980394, 0.99215686); +///
+pub const VIOLET_400: Srgba = Srgba::rgb(0.654902, 0.54509807, 0.98039216); +///
+pub const VIOLET_500: Srgba = Srgba::rgb(0.54509807, 0.36078432, 0.9647059); +///
+pub const VIOLET_600: Srgba = Srgba::rgb(0.4862745, 0.22745098, 0.92941177); +///
+pub const VIOLET_700: Srgba = Srgba::rgb(0.42745098, 0.15686275, 0.8509804); +///
+pub const VIOLET_800: Srgba = Srgba::rgb(0.35686275, 0.12941177, 0.7137255); +///
+pub const VIOLET_900: Srgba = Srgba::rgb(0.29803923, 0.11372549, 0.58431375); +///
+pub const VIOLET_950: Srgba = Srgba::rgb(0.18039216, 0.0627451, 0.39607844); + +///
+pub const YELLOW_50: Srgba = Srgba::rgb(0.99607843, 0.9882353, 0.9098039); +///
+pub const YELLOW_100: Srgba = Srgba::rgb(0.99607843, 0.9764706, 0.7647059); +///
+pub const YELLOW_200: Srgba = Srgba::rgb(0.99607843, 0.9411765, 0.5411765); +///
+pub const YELLOW_300: Srgba = Srgba::rgb(0.99215686, 0.8784314, 0.2784314); +///
+pub const YELLOW_400: Srgba = Srgba::rgb(0.98039216, 0.8, 0.08235294); +///
+pub const YELLOW_500: Srgba = Srgba::rgb(0.91764706, 0.7019608, 0.03137255); +///
+pub const YELLOW_600: Srgba = Srgba::rgb(0.7921569, 0.5411765, 0.015686275); +///
+pub const YELLOW_700: Srgba = Srgba::rgb(0.6313726, 0.38431373, 0.02745098); +///
+pub const YELLOW_800: Srgba = Srgba::rgb(0.52156866, 0.3019608, 0.05490196); +///
+pub const YELLOW_900: Srgba = Srgba::rgb(0.44313726, 0.24705882, 0.07058824); +///
+pub const YELLOW_950: Srgba = Srgba::rgb(0.25882354, 0.1254902, 0.023529412); + +///
+pub const ZINC_50: Srgba = Srgba::rgb(0.98039216, 0.98039216, 0.98039216); +///
+pub const ZINC_100: Srgba = Srgba::rgb(0.95686275, 0.95686275, 0.9607843); +///
+pub const ZINC_200: Srgba = Srgba::rgb(0.89411765, 0.89411765, 0.90588236); +///
+pub const ZINC_300: Srgba = Srgba::rgb(0.83137256, 0.83137256, 0.84705883); +///
+pub const ZINC_400: Srgba = Srgba::rgb(0.6313726, 0.6313726, 0.6666667); +///
+pub const ZINC_500: Srgba = Srgba::rgb(0.44313726, 0.44313726, 0.47843137); +///
+pub const ZINC_600: Srgba = Srgba::rgb(0.32156864, 0.32156864, 0.35686275); +///
+pub const ZINC_700: Srgba = Srgba::rgb(0.24705882, 0.24705882, 0.27450982); +///
+pub const ZINC_800: Srgba = Srgba::rgb(0.15294118, 0.15294118, 0.16470589); +///
+pub const ZINC_900: Srgba = Srgba::rgb(0.09411765, 0.09411765, 0.105882354); +///
+pub const ZINC_950: Srgba = Srgba::rgb(0.03529412, 0.03529412, 0.043137256); diff --git a/crates/bevy_color/src/xyza.rs b/crates/bevy_color/src/xyza.rs index caee5e705cbd5c..008b42fb12b248 100644 --- a/crates/bevy_color/src/xyza.rs +++ b/crates/bevy_color/src/xyza.rs @@ -1,4 +1,6 @@ -use crate::{Alpha, ClampColor, LinearRgba, Luminance, Mix, StandardColor}; +use crate::{ + impl_componentwise_point, Alpha, ClampColor, LinearRgba, Luminance, Mix, StandardColor, +}; use bevy_reflect::prelude::*; use serde::{Deserialize, Serialize}; @@ -22,6 +24,8 @@ pub struct Xyza { impl StandardColor for Xyza {} +impl_componentwise_point!(Xyza, [x, y, z, alpha]); + impl Xyza { /// Construct a new [`Xyza`] color from components. /// diff --git a/crates/bevy_core_pipeline/src/core_2d/camera_2d.rs b/crates/bevy_core_pipeline/src/core_2d/camera_2d.rs index 86e636d1a6b7d4..83cfdfe3b3a46d 100644 --- a/crates/bevy_core_pipeline/src/core_2d/camera_2d.rs +++ b/crates/bevy_core_pipeline/src/core_2d/camera_2d.rs @@ -22,6 +22,10 @@ pub struct Camera2d; pub struct Camera2dBundle { pub camera: Camera, pub camera_render_graph: CameraRenderGraph, + /// Note: default value for `OrthographicProjection.near` is `0.0` + /// which makes objects on the screen plane invisible to 2D camera. + /// `Camera2dBundle::default()` sets `near` to negative value, + /// so be careful when initializing this field manually. pub projection: OrthographicProjection, pub visible_entities: VisibleEntities, pub frustum: Frustum, diff --git a/crates/bevy_dev_tools/Cargo.toml b/crates/bevy_dev_tools/Cargo.toml index 7be25415831482..62ab2c8fcc8aee 100644 --- a/crates/bevy_dev_tools/Cargo.toml +++ b/crates/bevy_dev_tools/Cargo.toml @@ -9,22 +9,31 @@ license = "MIT OR Apache-2.0" keywords = ["bevy"] [features] +default = ["bevy_ui_debug"] bevy_ci_testing = ["serde", "ron"] +bevy_ui_debug = [] [dependencies] # bevy bevy_app = { path = "../bevy_app", version = "0.14.0-dev" } -bevy_utils = { path = "../bevy_utils", version = "0.14.0-dev" } +bevy_asset = { path = "../bevy_asset", version = "0.14.0-dev" } +bevy_color = { path = "../bevy_color", version = "0.14.0-dev" } +bevy_core = { path = "../bevy_core", version = "0.14.0-dev" } +bevy_core_pipeline = { path = "../bevy_core_pipeline", version = "0.14.0-dev" } +bevy_diagnostic = { path = "../bevy_diagnostic", version = "0.14.0-dev" } bevy_ecs = { path = "../bevy_ecs", version = "0.14.0-dev" } +bevy_gizmos = { path = "../bevy_gizmos", version = "0.14.0-dev" } +bevy_hierarchy = { path = "../bevy_hierarchy", version = "0.14.0-dev" } +bevy_input = { path = "../bevy_input", version = "0.14.0-dev" } +bevy_math = { path = "../bevy_math", version = "0.14.0-dev" } +bevy_reflect = { path = "../bevy_reflect", version = "0.14.0-dev" } bevy_render = { path = "../bevy_render", version = "0.14.0-dev" } bevy_time = { path = "../bevy_time", version = "0.14.0-dev" } -bevy_window = { path = "../bevy_window", version = "0.14.0-dev" } -bevy_asset = { path = "../bevy_asset", version = "0.14.0-dev" } +bevy_transform = { path = "../bevy_transform", version = "0.14.0-dev" } bevy_ui = { path = "../bevy_ui", version = "0.14.0-dev" } +bevy_utils = { path = "../bevy_utils", version = "0.14.0-dev" } +bevy_window = { path = "../bevy_window", version = "0.14.0-dev" } bevy_text = { path = "../bevy_text", version = "0.14.0-dev" } -bevy_diagnostic = { path = "../bevy_diagnostic", version = "0.14.0-dev" } -bevy_color = { path = "../bevy_color", version = "0.14.0-dev" } -bevy_input = { path = "../bevy_input", version = "0.14.0-dev" } # other serde = { version = "1.0", features = ["derive"], optional = true } diff --git a/crates/bevy_dev_tools/src/debug_overlay/inset.rs b/crates/bevy_dev_tools/src/debug_overlay/inset.rs new file mode 100644 index 00000000000000..86be2146c73d7b --- /dev/null +++ b/crates/bevy_dev_tools/src/debug_overlay/inset.rs @@ -0,0 +1,192 @@ +use bevy_color::Color; +use bevy_gizmos::{config::GizmoConfigGroup, prelude::Gizmos}; +use bevy_math::{Vec2, Vec2Swizzles}; +use bevy_reflect::Reflect; +use bevy_transform::prelude::GlobalTransform; +use bevy_utils::HashMap; + +use super::{CameraQuery, LayoutRect}; + +// Function used here so we don't need to redraw lines that are fairly close to each other. +fn approx_eq(compared: f32, other: f32) -> bool { + (compared - other).abs() < 0.001 +} + +fn rect_border_axis(rect: LayoutRect) -> (f32, f32, f32, f32) { + let pos = rect.pos; + let size = rect.size; + let offset = pos + size; + (pos.x, offset.x, pos.y, offset.y) +} + +#[derive(PartialEq, Eq, PartialOrd, Ord, Clone, Copy, Debug)] +enum Dir { + Start, + End, +} +impl Dir { + const fn increments(self) -> i64 { + match self { + Dir::Start => 1, + Dir::End => -1, + } + } +} +impl From for Dir { + fn from(value: i64) -> Self { + if value.is_positive() { + Dir::Start + } else { + Dir::End + } + } +} +/// Collection of axis aligned "lines" (actually just their coordinate on +/// a given axis). +#[derive(Debug, Clone)] +struct DrawnLines { + lines: HashMap, + width: f32, +} +#[allow(clippy::cast_precision_loss, clippy::cast_possible_truncation)] +impl DrawnLines { + fn new(width: f32) -> Self { + DrawnLines { + lines: HashMap::new(), + width, + } + } + /// Return `value` offset by as many `increment`s as necessary to make it + /// not overlap with already drawn lines. + fn inset(&self, value: f32) -> f32 { + let scaled = value / self.width; + let fract = scaled.fract(); + let mut on_grid = scaled.floor() as i64; + for _ in 0..10 { + let Some(dir) = self.lines.get(&on_grid) else { + break; + }; + // TODO(clean): This fixes a panic, but I'm not sure how valid this is + let Some(added) = on_grid.checked_add(dir.increments()) else { + break; + }; + on_grid = added; + } + ((on_grid as f32) + fract) * self.width + } + /// Remove a line from the collection of drawn lines. + /// + /// Typically, we only care for pre-existing lines when drawing the children + /// of a container, nothing more. So we remove it after we are done with + /// the children. + fn remove(&mut self, value: f32, increment: i64) { + let mut on_grid = (value / self.width).floor() as i64; + loop { + // TODO(clean): This fixes a panic, but I'm not sure how valid this is + let Some(next_cell) = on_grid.checked_add(increment) else { + return; + }; + if !self.lines.contains_key(&next_cell) { + self.lines.remove(&on_grid); + return; + } + on_grid = next_cell; + } + } + /// Add a line from the collection of drawn lines. + fn add(&mut self, value: f32, increment: i64) { + let mut on_grid = (value / self.width).floor() as i64; + loop { + let old_value = self.lines.insert(on_grid, increment.into()); + if old_value.is_none() { + return; + } + // TODO(clean): This fixes a panic, but I'm not sure how valid this is + let Some(added) = on_grid.checked_add(increment) else { + return; + }; + on_grid = added; + } + } +} + +#[derive(GizmoConfigGroup, Reflect, Default)] +pub struct UiGizmosDebug; + +pub(super) struct InsetGizmo<'w, 's> { + draw: Gizmos<'w, 's, UiGizmosDebug>, + cam: CameraQuery<'w, 's>, + known_y: DrawnLines, + known_x: DrawnLines, +} +impl<'w, 's> InsetGizmo<'w, 's> { + pub(super) fn new( + draw: Gizmos<'w, 's, UiGizmosDebug>, + cam: CameraQuery<'w, 's>, + line_width: f32, + ) -> Self { + InsetGizmo { + draw, + cam, + known_y: DrawnLines::new(line_width), + known_x: DrawnLines::new(line_width), + } + } + fn relative(&self, mut position: Vec2) -> Vec2 { + let zero = GlobalTransform::IDENTITY; + let Ok(cam) = self.cam.get_single() else { + return Vec2::ZERO; + }; + if let Some(new_position) = cam.world_to_viewport(&zero, position.extend(0.)) { + position = new_position; + }; + position.xy() + } + fn line_2d(&mut self, mut start: Vec2, mut end: Vec2, color: Color) { + if approx_eq(start.x, end.x) { + start.x = self.known_x.inset(start.x); + end.x = start.x; + } else if approx_eq(start.y, end.y) { + start.y = self.known_y.inset(start.y); + end.y = start.y; + } + let (start, end) = (self.relative(start), self.relative(end)); + self.draw.line_2d(start, end, color); + } + pub(super) fn set_scope(&mut self, rect: LayoutRect) { + let (left, right, top, bottom) = rect_border_axis(rect); + self.known_x.add(left, 1); + self.known_x.add(right, -1); + self.known_y.add(top, 1); + self.known_y.add(bottom, -1); + } + pub(super) fn clear_scope(&mut self, rect: LayoutRect) { + let (left, right, top, bottom) = rect_border_axis(rect); + self.known_x.remove(left, 1); + self.known_x.remove(right, -1); + self.known_y.remove(top, 1); + self.known_y.remove(bottom, -1); + } + pub(super) fn rect_2d(&mut self, rect: LayoutRect, color: Color) { + let (left, right, top, bottom) = rect_border_axis(rect); + if approx_eq(left, right) { + self.line_2d(Vec2::new(left, top), Vec2::new(left, bottom), color); + } else if approx_eq(top, bottom) { + self.line_2d(Vec2::new(left, top), Vec2::new(right, top), color); + } else { + let inset_x = |v| self.known_x.inset(v); + let inset_y = |v| self.known_y.inset(v); + let (left, right) = (inset_x(left), inset_x(right)); + let (top, bottom) = (inset_y(top), inset_y(bottom)); + let strip = [ + Vec2::new(left, top), + Vec2::new(left, bottom), + Vec2::new(right, bottom), + Vec2::new(right, top), + Vec2::new(left, top), + ]; + self.draw + .linestrip_2d(strip.map(|v| self.relative(v)), color); + } + } +} diff --git a/crates/bevy_dev_tools/src/debug_overlay/mod.rs b/crates/bevy_dev_tools/src/debug_overlay/mod.rs new file mode 100644 index 00000000000000..952539850638d7 --- /dev/null +++ b/crates/bevy_dev_tools/src/debug_overlay/mod.rs @@ -0,0 +1,280 @@ +//! A visual representation of UI node sizes. +use std::any::{Any, TypeId}; + +use bevy_app::{App, Plugin, PostUpdate}; +use bevy_color::Hsla; +use bevy_core::Name; +use bevy_core_pipeline::core_2d::Camera2dBundle; +use bevy_ecs::{prelude::*, system::SystemParam}; +use bevy_gizmos::{config::GizmoConfigStore, prelude::Gizmos, AppGizmoBuilder}; +use bevy_hierarchy::{Children, Parent}; +use bevy_math::{Vec2, Vec3Swizzles}; +use bevy_render::{ + camera::RenderTarget, + prelude::*, + view::{RenderLayers, VisibilitySystems}, +}; +use bevy_transform::{prelude::GlobalTransform, TransformSystem}; +use bevy_ui::{DefaultUiCamera, Display, Node, Style, TargetCamera, UiScale}; +use bevy_utils::{default, warn_once}; +use bevy_window::{PrimaryWindow, Window, WindowRef}; + +use inset::InsetGizmo; + +use self::inset::UiGizmosDebug; + +mod inset; + +/// The [`Camera::order`] index used by the layout debug camera. +pub const LAYOUT_DEBUG_CAMERA_ORDER: isize = 255; +/// The [`RenderLayers`] used by the debug gizmos and the debug camera. +pub const LAYOUT_DEBUG_LAYERS: RenderLayers = RenderLayers::none().with(16); + +#[derive(Clone, Copy)] +struct LayoutRect { + pos: Vec2, + size: Vec2, +} + +impl LayoutRect { + fn new(trans: &GlobalTransform, node: &Node, scale: f32) -> Self { + let mut this = Self { + pos: trans.translation().xy() * scale, + size: node.size() * scale, + }; + this.pos -= this.size / 2.; + this + } +} + +#[derive(Component, Debug, Clone, Default)] +struct DebugOverlayCamera; + +/// The debug overlay options. +#[derive(Resource, Clone, Default)] +pub struct UiDebugOptions { + /// Whether the overlay is enabled. + pub enabled: bool, + layout_gizmos_camera: Option, +} +impl UiDebugOptions { + /// This will toggle the enabled field, setting it to false if true and true if false. + pub fn toggle(&mut self) { + self.enabled = !self.enabled; + } +} + +/// The system responsible to change the [`Camera`] config based on changes in [`UiDebugOptions`] and [`GizmoConfig`](bevy_gizmos::prelude::GizmoConfig). +fn update_debug_camera( + mut gizmo_config: ResMut, + mut options: ResMut, + mut cmds: Commands, + mut debug_cams: Query<&mut Camera, With>, +) { + if !options.is_changed() && !gizmo_config.is_changed() { + return; + } + if !options.enabled { + let Some(cam) = options.layout_gizmos_camera else { + return; + }; + let Ok(mut cam) = debug_cams.get_mut(cam) else { + return; + }; + cam.is_active = false; + if let Some((config, _)) = gizmo_config.get_config_mut_dyn(&TypeId::of::()) { + config.enabled = false; + } + } else { + let spawn_cam = || { + cmds.spawn(( + Camera2dBundle { + projection: OrthographicProjection { + far: 1000.0, + viewport_origin: Vec2::new(0.0, 0.0), + ..default() + }, + camera: Camera { + order: LAYOUT_DEBUG_CAMERA_ORDER, + clear_color: ClearColorConfig::None, + ..default() + }, + ..default() + }, + LAYOUT_DEBUG_LAYERS, + DebugOverlayCamera, + Name::new("Layout Debug Camera"), + )) + .id() + }; + if let Some((config, _)) = gizmo_config.get_config_mut_dyn(&TypeId::of::()) { + config.enabled = true; + config.render_layers = LAYOUT_DEBUG_LAYERS; + } + let cam = *options.layout_gizmos_camera.get_or_insert_with(spawn_cam); + let Ok(mut cam) = debug_cams.get_mut(cam) else { + return; + }; + cam.is_active = true; + } +} + +/// The function that goes over every children of given [`Entity`], skipping the not visible ones and drawing the gizmos outlines. +fn outline_nodes(outline: &OutlineParam, draw: &mut InsetGizmo, this_entity: Entity, scale: f32) { + let Ok(to_iter) = outline.children.get(this_entity) else { + return; + }; + + for (entity, trans, node, style, children) in outline.nodes.iter_many(to_iter) { + if style.is_none() || style.is_some_and(|s| matches!(s.display, Display::None)) { + continue; + } + + if let Ok(view_visibility) = outline.view_visibility.get(entity) { + if !view_visibility.get() { + continue; + } + } + let rect = LayoutRect::new(trans, node, scale); + outline_node(entity, rect, draw); + if children.is_some() { + outline_nodes(outline, draw, entity, scale); + } + draw.clear_scope(rect); + } +} + +type NodesQuery = ( + Entity, + &'static GlobalTransform, + &'static Node, + Option<&'static Style>, + Option<&'static Children>, +); + +#[derive(SystemParam)] +struct OutlineParam<'w, 's> { + gizmo_config: Res<'w, GizmoConfigStore>, + children: Query<'w, 's, &'static Children>, + nodes: Query<'w, 's, NodesQuery>, + view_visibility: Query<'w, 's, &'static ViewVisibility>, + ui_scale: Res<'w, UiScale>, +} + +type CameraQuery<'w, 's> = Query<'w, 's, &'static Camera, With>; + +#[derive(SystemParam)] +struct CameraParam<'w, 's> { + debug_camera: Query<'w, 's, &'static Camera, With>, + cameras: Query<'w, 's, &'static Camera, Without>, + primary_window: Query<'w, 's, &'static Window, With>, + default_ui_camera: DefaultUiCamera<'w, 's>, +} + +/// system responsible for drawing the gizmos lines around all the node roots, iterating recursively through all visible children. +fn outline_roots( + outline: OutlineParam, + draw: Gizmos, + cam: CameraParam, + roots: Query< + ( + Entity, + &GlobalTransform, + &Node, + Option<&ViewVisibility>, + Option<&TargetCamera>, + ), + Without, + >, + window: Query<&Window, With>, + nonprimary_windows: Query<&Window, Without>, + options: Res, +) { + if !options.enabled { + return; + } + if !nonprimary_windows.is_empty() { + warn_once!( + "The layout debug view only uses the primary window scale, \ + you might notice gaps between container lines" + ); + } + let window_scale = window.get_single().map_or(1., Window::scale_factor); + let scale_factor = window_scale * outline.ui_scale.0; + + // We let the line be defined by the window scale alone + let line_width = outline + .gizmo_config + .get_config_dyn(&UiGizmosDebug.type_id()) + .map_or(2., |(config, _)| config.line_width) + / window_scale; + let mut draw = InsetGizmo::new(draw, cam.debug_camera, line_width); + for (entity, trans, node, view_visibility, maybe_target_camera) in &roots { + if let Some(view_visibility) = view_visibility { + // If the entity isn't visible, we will not draw any lines. + if !view_visibility.get() { + continue; + } + } + // We skip ui in other windows that are not the primary one + if let Some(camera_entity) = maybe_target_camera + .map(|target| target.0) + .or(cam.default_ui_camera.get()) + { + let Ok(camera) = cam.cameras.get(camera_entity) else { + // The camera wasn't found. Either the Camera don't exist or the Camera is the debug Camera, that we want to skip and warn + warn_once!("Camera {:?} wasn't found for debug overlay", camera_entity); + continue; + }; + match camera.target { + RenderTarget::Window(window_ref) => { + if let WindowRef::Entity(window_entity) = window_ref { + if cam.primary_window.get(window_entity).is_err() { + // This window isn't the primary, so we skip this root. + continue; + } + } + } + // Hard to know the results of this, better skip this target. + _ => continue, + } + } + + let rect = LayoutRect::new(trans, node, scale_factor); + outline_node(entity, rect, &mut draw); + outline_nodes(&outline, &mut draw, entity, scale_factor); + } +} + +/// Function responsible for drawing the gizmos lines around the given Entity +fn outline_node(entity: Entity, rect: LayoutRect, draw: &mut InsetGizmo) { + let color = Hsla::sequential_dispersed(entity.index()); + + draw.rect_2d(rect, color.into()); + draw.set_scope(rect); +} + +/// The debug overlay plugin. +/// +/// This spawns a new camera with a low order, and draws gizmo. +/// +/// Note that due to limitation with [`bevy_gizmos`], multiple windows with this feature +/// enabled isn't supported and the lines are only drawn in the [`PrimaryWindow`] +pub struct DebugUiPlugin; +impl Plugin for DebugUiPlugin { + fn build(&self, app: &mut App) { + app.init_resource::() + .init_gizmo_group::() + .add_systems( + PostUpdate, + ( + update_debug_camera, + outline_roots + .after(TransformSystem::TransformPropagate) + // This needs to run before VisibilityPropagate so it can relies on ViewVisibility + .before(VisibilitySystems::VisibilityPropagate), + ) + .chain(), + ); + } +} diff --git a/crates/bevy_dev_tools/src/fps_overlay.rs b/crates/bevy_dev_tools/src/fps_overlay.rs index 51b8e7886dcf96..d3f57f998411b1 100644 --- a/crates/bevy_dev_tools/src/fps_overlay.rs +++ b/crates/bevy_dev_tools/src/fps_overlay.rs @@ -10,8 +10,18 @@ use bevy_ecs::{ schedule::{common_conditions::resource_changed, IntoSystemConfigs}, system::{Commands, Query, Res, Resource}, }; +use bevy_hierarchy::BuildChildren; use bevy_text::{Font, Text, TextSection, TextStyle}; -use bevy_ui::node_bundles::TextBundle; +use bevy_ui::{ + node_bundles::{NodeBundle, TextBundle}, + PositionType, Style, ZIndex, +}; +use bevy_utils::default; + +/// Global [`ZIndex`] used to render the fps overlay. +/// +/// We use a number slightly under `i32::MAX` so you can render on top of it if you really need to. +pub const FPS_OVERLAY_ZINDEX: i32 = i32::MAX - 32; /// A plugin that adds an FPS overlay to the Bevy application. /// @@ -67,13 +77,26 @@ impl Default for FpsOverlayConfig { struct FpsText; fn setup(mut commands: Commands, overlay_config: Res) { - commands.spawn(( - TextBundle::from_sections([ - TextSection::new("FPS: ", overlay_config.text_config.clone()), - TextSection::from_style(overlay_config.text_config.clone()), - ]), - FpsText, - )); + commands + .spawn(NodeBundle { + style: Style { + // We need to make sure the overlay doesn't affect the position of other UI nodes + position_type: PositionType::Absolute, + ..default() + }, + // Render overlay on top of everything + z_index: ZIndex::Global(FPS_OVERLAY_ZINDEX), + ..default() + }) + .with_children(|c| { + c.spawn(( + TextBundle::from_sections([ + TextSection::new("FPS: ", overlay_config.text_config.clone()), + TextSection::from_style(overlay_config.text_config.clone()), + ]), + FpsText, + )); + }); } fn update_text(diagnostic: Res, mut query: Query<&mut Text, With>) { diff --git a/crates/bevy_dev_tools/src/lib.rs b/crates/bevy_dev_tools/src/lib.rs index adad8cec9030bf..8bb9b0b7d2b987 100644 --- a/crates/bevy_dev_tools/src/lib.rs +++ b/crates/bevy_dev_tools/src/lib.rs @@ -8,6 +8,9 @@ use bevy_app::prelude::*; pub mod ci_testing; pub mod fps_overlay; +#[cfg(feature = "bevy_ui_debug")] +pub mod debug_overlay; + /// Enables developer tools in an [`App`]. This plugin is added automatically with `bevy_dev_tools` /// feature. /// diff --git a/crates/bevy_gltf/src/loader.rs b/crates/bevy_gltf/src/loader.rs index 6f2f27969805c6..1a8caa4531a955 100644 --- a/crates/bevy_gltf/src/loader.rs +++ b/crates/bevy_gltf/src/loader.rs @@ -162,17 +162,15 @@ impl AssetLoader for GltfLoader { type Asset = Gltf; type Settings = GltfLoaderSettings; type Error = GltfError; - fn load<'a>( + async fn load<'a>( &'a self, - reader: &'a mut Reader, + reader: &'a mut Reader<'_>, settings: &'a GltfLoaderSettings, - load_context: &'a mut LoadContext, - ) -> bevy_utils::BoxedFuture<'a, Result> { - Box::pin(async move { - let mut bytes = Vec::new(); - reader.read_to_end(&mut bytes).await?; - load_gltf(self, &bytes, load_context, settings).await - }) + load_context: &'a mut LoadContext<'_>, + ) -> Result { + let mut bytes = Vec::new(); + reader.read_to_end(&mut bytes).await?; + load_gltf(self, &bytes, load_context, settings).await } fn extensions(&self) -> &[&str] { diff --git a/crates/bevy_internal/Cargo.toml b/crates/bevy_internal/Cargo.toml index 9aea3420a7144b..fc7eb39ff1ae9e 100644 --- a/crates/bevy_internal/Cargo.toml +++ b/crates/bevy_internal/Cargo.toml @@ -175,6 +175,7 @@ bevy_ecs = { path = "../bevy_ecs", version = "0.14.0-dev" } bevy_hierarchy = { path = "../bevy_hierarchy", version = "0.14.0-dev" } bevy_input = { path = "../bevy_input", version = "0.14.0-dev" } bevy_log = { path = "../bevy_log", version = "0.14.0-dev" } +bevy_panic_handler = { path = "../bevy_panic_handler", version = "0.14.0-dev" } bevy_math = { path = "../bevy_math", version = "0.14.0-dev" } bevy_ptr = { path = "../bevy_ptr", version = "0.14.0-dev" } bevy_reflect = { path = "../bevy_reflect", version = "0.14.0-dev", features = [ diff --git a/crates/bevy_internal/src/default_plugins.rs b/crates/bevy_internal/src/default_plugins.rs index dcea35be338e32..fdc33428e58eb8 100644 --- a/crates/bevy_internal/src/default_plugins.rs +++ b/crates/bevy_internal/src/default_plugins.rs @@ -1,6 +1,7 @@ use bevy_app::{Plugin, PluginGroup, PluginGroupBuilder}; /// This plugin group will add all the default plugins for a *Bevy* application: +/// * [`PanicHandlerPlugin`](crate::panic_handler::PanicHandlerPlugin) /// * [`LogPlugin`](crate::log::LogPlugin) /// * [`TaskPoolPlugin`](crate::core::TaskPoolPlugin) /// * [`TypeRegistrationPlugin`](crate::core::TypeRegistrationPlugin) @@ -42,6 +43,7 @@ impl PluginGroup for DefaultPlugins { fn build(self) -> PluginGroupBuilder { let mut group = PluginGroupBuilder::start::(); group = group + .add(bevy_panic_handler::PanicHandlerPlugin) .add(bevy_log::LogPlugin::default()) .add(bevy_core::TaskPoolPlugin::default()) .add(bevy_core::TypeRegistrationPlugin) diff --git a/crates/bevy_internal/src/lib.rs b/crates/bevy_internal/src/lib.rs index 896091c404c983..e7a46f246c8089 100644 --- a/crates/bevy_internal/src/lib.rs +++ b/crates/bevy_internal/src/lib.rs @@ -53,6 +53,11 @@ pub mod log { pub use bevy_log::*; } +pub mod panic_handler { + //! Platform-specific panic handlers + pub use bevy_panic_handler::*; +} + pub mod math { //! Math types (Vec3, Mat4, Quat, etc) and helpers. pub use bevy_math::*; diff --git a/crates/bevy_log/Cargo.toml b/crates/bevy_log/Cargo.toml index a5229d61ac9347..eaf53d1f6cdeb9 100644 --- a/crates/bevy_log/Cargo.toml +++ b/crates/bevy_log/Cargo.toml @@ -34,7 +34,6 @@ tracy-client = { version = "0.17.0", optional = true } android_log-sys = "0.3.0" [target.'cfg(target_arch = "wasm32")'.dependencies] -console_error_panic_hook = "0.1.6" tracing-wasm = "0.2.1" [lints] diff --git a/crates/bevy_log/src/lib.rs b/crates/bevy_log/src/lib.rs index eb3cef046a4fff..b6576acb172d94 100644 --- a/crates/bevy_log/src/lib.rs +++ b/crates/bevy_log/src/lib.rs @@ -205,7 +205,6 @@ impl Plugin for LogPlugin { #[cfg(target_arch = "wasm32")] { - console_error_panic_hook::set_once(); finished_subscriber = subscriber.with(tracing_wasm::WASMLayer::new( tracing_wasm::WASMLayerConfig::default(), )); diff --git a/crates/bevy_math/src/direction.rs b/crates/bevy_math/src/direction.rs index c98736960c8401..150a9105b86e1d 100644 --- a/crates/bevy_math/src/direction.rs +++ b/crates/bevy_math/src/direction.rs @@ -136,6 +136,11 @@ impl Dir2 { pub fn from_xy(x: f32, y: f32) -> Result { Self::new(Vec2::new(x, y)) } + + /// Returns the inner [`Vec2`] + pub const fn as_vec2(&self) -> Vec2 { + self.0 + } } impl TryFrom for Dir2 { @@ -146,6 +151,12 @@ impl TryFrom for Dir2 { } } +impl From for Vec2 { + fn from(value: Dir2) -> Self { + value.as_vec2() + } +} + impl std::ops::Deref for Dir2 { type Target = Vec2; fn deref(&self) -> &Self::Target { @@ -287,6 +298,11 @@ impl Dir3 { pub fn from_xyz(x: f32, y: f32, z: f32) -> Result { Self::new(Vec3::new(x, y, z)) } + + /// Returns the inner [`Vec3`] + pub const fn as_vec3(&self) -> Vec3 { + self.0 + } } impl TryFrom for Dir3 { @@ -447,6 +463,11 @@ impl Dir3A { pub fn from_xyz(x: f32, y: f32, z: f32) -> Result { Self::new(Vec3A::new(x, y, z)) } + + /// Returns the inner [`Vec3A`] + pub const fn as_vec3a(&self) -> Vec3A { + self.0 + } } impl TryFrom for Dir3A { diff --git a/crates/bevy_panic_handler/Cargo.toml b/crates/bevy_panic_handler/Cargo.toml new file mode 100644 index 00000000000000..24b96de3c2a4a7 --- /dev/null +++ b/crates/bevy_panic_handler/Cargo.toml @@ -0,0 +1,23 @@ +[package] +name = "bevy_panic_handler" +version = "0.14.0-dev" +edition = "2021" +description = "Provides panic handlers for Bevy Engine" +homepage = "https://bevyengine.org" +repository = "https://github.com/bevyengine/bevy" +license = "MIT OR Apache-2.0" +keywords = ["bevy"] + +[features] + +[dependencies] +bevy_app = { path = "../bevy_app", version = "0.14.0-dev" } + +[target.'cfg(target_arch = "wasm32")'.dependencies] +console_error_panic_hook = "0.1.6" + +[lints] +workspace = true + +[package.metadata.docs.rs] +all-features = true diff --git a/crates/bevy_panic_handler/src/lib.rs b/crates/bevy_panic_handler/src/lib.rs new file mode 100644 index 00000000000000..8e3705980de626 --- /dev/null +++ b/crates/bevy_panic_handler/src/lib.rs @@ -0,0 +1,54 @@ +//! This crate provides panic handlers for [Bevy](https://bevyengine.org) +//! apps, and automatically configures platform specifics (i.e. WASM or Android). +//! +//! By default, the [`PanicHandlerPlugin`] from this crate is included in Bevy's `DefaultPlugins`. +//! +//! For more fine-tuned control over panic behavior, disable the [`PanicHandlerPlugin`] or +//! `DefaultPlugins` during app initialization. +#![cfg_attr(docsrs, feature(doc_auto_cfg))] + +use bevy_app::{App, Plugin}; + +/// Adds sensible panic handlers to Apps. This plugin is part of the `DefaultPlugins`. Adding +/// this plugin will setup a panic hook appropriate to your target platform: +/// * On WASM, uses [`console_error_panic_hook`](https://crates.io/crates/console_error_panic_hook), logging +/// to the browser console. +/// * Other platforms are currently not setup. +/// +/// ```no_run +/// # use bevy_app::{App, NoopPluginGroup as MinimalPlugins, PluginGroup}; +/// # use bevy_panic_handler::PanicHandlerPlugin; +/// fn main() { +/// App::new() +/// .add_plugins(MinimalPlugins) +/// .add_plugins(PanicHandlerPlugin) +/// .run(); +/// } +/// ``` +/// +/// If you want to setup your own panic handler, you should disable this +/// plugin from `DefaultPlugins`: +/// ```no_run +/// # use bevy_app::{App, NoopPluginGroup as DefaultPlugins, PluginGroup}; +/// # use bevy_panic_handler::PanicHandlerPlugin; +/// fn main() { +/// App::new() +/// .add_plugins(DefaultPlugins.build().disable::()) +/// .run(); +/// } +/// ``` +#[derive(Default)] +pub struct PanicHandlerPlugin; + +impl Plugin for PanicHandlerPlugin { + fn build(&self, _app: &mut App) { + #[cfg(target_arch = "wasm32")] + { + console_error_panic_hook::set_once(); + } + #[cfg(not(target_arch = "wasm32"))] + { + // Use the default target panic hook - Do nothing. + } + } +} diff --git a/crates/bevy_pbr/src/material.rs b/crates/bevy_pbr/src/material.rs index 59d0e7b67bea7a..96c32bfd6834cc 100644 --- a/crates/bevy_pbr/src/material.rs +++ b/crates/bevy_pbr/src/material.rs @@ -193,6 +193,8 @@ pub struct MaterialPlugin { /// When it is enabled, it will automatically add the [`PrepassPlugin`] /// required to make the prepass work on this Material. pub prepass_enabled: bool, + /// Controls if shadows are enabled for the Material. + pub shadows_enabled: bool, pub _marker: PhantomData, } @@ -200,6 +202,7 @@ impl Default for MaterialPlugin { fn default() -> Self { Self { prepass_enabled: true, + shadows_enabled: true, _marker: Default::default(), } } @@ -231,18 +234,26 @@ where prepare_materials:: .in_set(RenderSet::PrepareAssets) .after(prepare_assets::), - queue_shadows:: - .in_set(RenderSet::QueueMeshes) - .after(prepare_materials::), queue_material_meshes:: .in_set(RenderSet::QueueMeshes) .after(prepare_materials::), ), ); + + if self.shadows_enabled { + render_app.add_systems( + Render, + (queue_shadows:: + .in_set(RenderSet::QueueMeshes) + .after(prepare_materials::),), + ); + } } - // PrepassPipelinePlugin is required for shadow mapping and the optional PrepassPlugin - app.add_plugins(PrepassPipelinePlugin::::default()); + if self.shadows_enabled || self.prepass_enabled { + // PrepassPipelinePlugin is required for shadow mapping and the optional PrepassPlugin + app.add_plugins(PrepassPipelinePlugin::::default()); + } if self.prepass_enabled { app.add_plugins(PrepassPlugin::::default()); diff --git a/crates/bevy_render/macros/src/as_bind_group.rs b/crates/bevy_render/macros/src/as_bind_group.rs index b1c7a12e0b0bc0..06ce6f7982810a 100644 --- a/crates/bevy_render/macros/src/as_bind_group.rs +++ b/crates/bevy_render/macros/src/as_bind_group.rs @@ -272,7 +272,8 @@ pub fn derive_as_bind_group(ast: syn::DeriveInput) -> Result { let fallback_image = get_fallback_image(&render_path, dimension); - binding_impls.push(quote! { + // insert fallible texture-based entries at 0 so that if we fail here, we exit before allocating any buffers + binding_impls.insert(0, quote! { ( #binding_index, #render_path::render_resource::OwnedBindingResource::TextureView({ let handle: Option<&#asset_path::Handle<#render_path::texture::Image>> = (&self.#field_name).into(); @@ -311,7 +312,8 @@ pub fn derive_as_bind_group(ast: syn::DeriveInput) -> Result { let fallback_image = get_fallback_image(&render_path, *dimension); - binding_impls.push(quote! { + // insert fallible texture-based entries at 0 so that if we fail here, we exit before allocating any buffers + binding_impls.insert(0, quote! { ( #binding_index, #render_path::render_resource::OwnedBindingResource::TextureView({ @@ -353,7 +355,8 @@ pub fn derive_as_bind_group(ast: syn::DeriveInput) -> Result { let fallback_image = get_fallback_image(&render_path, *dimension); - binding_impls.push(quote! { + // insert fallible texture-based entries at 0 so that if we fail here, we exit before allocating any buffers + binding_impls.insert(0, quote! { ( #binding_index, #render_path::render_resource::OwnedBindingResource::Sampler({ diff --git a/crates/bevy_render/src/camera/camera.rs b/crates/bevy_render/src/camera/camera.rs index ca22a773dc7674..728a863b703cfd 100644 --- a/crates/bevy_render/src/camera/camera.rs +++ b/crates/bevy_render/src/camera/camera.rs @@ -88,7 +88,7 @@ pub struct ComputedCameraValues { /// /// #[derive(Component, Clone, Copy, Reflect)] -#[reflect_value(Component)] +#[reflect_value(Component, Default)] pub struct Exposure { /// pub ev100: f32, @@ -184,7 +184,7 @@ impl Default for PhysicalCameraParameters { /// Adding a camera is typically done by adding a bundle, either the `Camera2dBundle` or the /// `Camera3dBundle`. #[derive(Component, Debug, Reflect, Clone)] -#[reflect(Component)] +#[reflect(Component, Default)] pub struct Camera { /// If set, this camera will render to the given [`Viewport`] rectangle within the configured [`RenderTarget`]. pub viewport: Option, @@ -771,7 +771,7 @@ pub fn camera_system( /// This component lets you control the [`TextureUsages`] field of the main texture generated for the camera #[derive(Component, ExtractComponent, Clone, Copy, Reflect)] -#[reflect_value(Component)] +#[reflect_value(Component, Default)] pub struct CameraMainTextureUsages(pub TextureUsages); impl Default for CameraMainTextureUsages { fn default() -> Self { diff --git a/crates/bevy_render/src/camera/clear_color.rs b/crates/bevy_render/src/camera/clear_color.rs index 02b74ce46c1a3a..e986b3d30b29ae 100644 --- a/crates/bevy_render/src/camera/clear_color.rs +++ b/crates/bevy_render/src/camera/clear_color.rs @@ -2,12 +2,12 @@ use crate::extract_resource::ExtractResource; use bevy_color::Color; use bevy_derive::{Deref, DerefMut}; use bevy_ecs::prelude::*; -use bevy_reflect::{Reflect, ReflectDeserialize, ReflectSerialize}; +use bevy_reflect::prelude::*; use serde::{Deserialize, Serialize}; /// For a camera, specifies the color used to clear the viewport before rendering. #[derive(Reflect, Serialize, Deserialize, Clone, Debug, Default)] -#[reflect(Serialize, Deserialize)] +#[reflect(Serialize, Deserialize, Default)] pub enum ClearColorConfig { /// The clear color is taken from the world's [`ClearColor`] resource. #[default] @@ -31,7 +31,7 @@ impl From for ClearColorConfig { /// This color appears as the "background" color for simple apps, /// when there are portions of the screen with nothing rendered. #[derive(Resource, Clone, Debug, Deref, DerefMut, ExtractResource, Reflect)] -#[reflect(Resource)] +#[reflect(Resource, Default)] pub struct ClearColor(pub Color); /// Match the dark gray bevy website code block color by default. diff --git a/crates/bevy_render/src/globals.rs b/crates/bevy_render/src/globals.rs index d1a7df0b5e31a6..11f0c5295efff5 100644 --- a/crates/bevy_render/src/globals.rs +++ b/crates/bevy_render/src/globals.rs @@ -9,7 +9,7 @@ use bevy_app::{App, Plugin}; use bevy_asset::{load_internal_asset, Handle}; use bevy_core::FrameCount; use bevy_ecs::prelude::*; -use bevy_reflect::Reflect; +use bevy_reflect::prelude::*; use bevy_time::Time; pub const GLOBALS_TYPE_HANDLE: Handle = Handle::weak_from_u128(17924628719070609599); @@ -45,7 +45,7 @@ fn extract_time(mut commands: Commands, time: Extract>) { /// Contains global values useful when writing shaders. /// Currently only contains values related to time. #[derive(Default, Clone, Resource, ExtractResource, Reflect, ShaderType)] -#[reflect(Resource)] +#[reflect(Resource, Default)] pub struct GlobalsUniform { /// The time since startup in seconds. /// Wraps to 0 after 1 hour. diff --git a/crates/bevy_render/src/mesh/mesh/skinning.rs b/crates/bevy_render/src/mesh/mesh/skinning.rs index 616f2a5472abf7..f1605ec74b7357 100644 --- a/crates/bevy_render/src/mesh/mesh/skinning.rs +++ b/crates/bevy_render/src/mesh/mesh/skinning.rs @@ -6,11 +6,11 @@ use bevy_ecs::{ reflect::ReflectMapEntities, }; use bevy_math::Mat4; -use bevy_reflect::{Reflect, TypePath}; +use bevy_reflect::prelude::*; use std::ops::Deref; #[derive(Component, Debug, Default, Clone, Reflect)] -#[reflect(Component, MapEntities)] +#[reflect(Component, MapEntities, Default)] pub struct SkinnedMesh { pub inverse_bindposes: Handle, pub joints: Vec, diff --git a/crates/bevy_render/src/mesh/morph.rs b/crates/bevy_render/src/mesh/morph.rs index 74430beffc7c80..b5eac7cdfec63e 100644 --- a/crates/bevy_render/src/mesh/morph.rs +++ b/crates/bevy_render/src/mesh/morph.rs @@ -9,7 +9,7 @@ use bevy_asset::Handle; use bevy_ecs::prelude::*; use bevy_hierarchy::Children; use bevy_math::Vec3; -use bevy_reflect::Reflect; +use bevy_reflect::prelude::*; use bytemuck::{Pod, Zeroable}; use std::{iter, mem}; use thiserror::Error; @@ -128,7 +128,7 @@ impl MorphTargetImage { /// /// [morph targets]: https://en.wikipedia.org/wiki/Morph_target_animation #[derive(Reflect, Default, Debug, Clone, Component)] -#[reflect(Debug, Component)] +#[reflect(Debug, Component, Default)] pub struct MorphWeights { weights: Vec, /// The first mesh primitive assigned to these weights @@ -173,7 +173,7 @@ impl MorphWeights { /// /// [morph targets]: https://en.wikipedia.org/wiki/Morph_target_animation #[derive(Reflect, Default, Debug, Clone, Component)] -#[reflect(Debug, Component)] +#[reflect(Debug, Component, Default)] pub struct MeshMorphWeights { weights: Vec, } diff --git a/crates/bevy_render/src/primitives/mod.rs b/crates/bevy_render/src/primitives/mod.rs index 25fef4abd2c09f..d5796b4a7ff70d 100644 --- a/crates/bevy_render/src/primitives/mod.rs +++ b/crates/bevy_render/src/primitives/mod.rs @@ -2,7 +2,7 @@ use std::borrow::Borrow; use bevy_ecs::{component::Component, entity::EntityHashMap, reflect::ReflectComponent}; use bevy_math::{Affine3A, Mat3A, Mat4, Vec3, Vec3A, Vec4, Vec4Swizzles}; -use bevy_reflect::Reflect; +use bevy_reflect::prelude::*; /// An axis-aligned bounding box, defined by: /// - a center, @@ -31,7 +31,7 @@ use bevy_reflect::Reflect; /// [`Mesh`]: crate::mesh::Mesh /// [`Handle`]: crate::mesh::Mesh #[derive(Component, Clone, Copy, Debug, Default, Reflect, PartialEq)] -#[reflect(Component)] +#[reflect(Component, Default)] pub struct Aabb { pub center: Vec3A, pub half_extents: Vec3A, @@ -212,7 +212,7 @@ impl HalfSpace { /// [`CameraProjection`]: crate::camera::CameraProjection /// [`GlobalTransform`]: bevy_transform::components::GlobalTransform #[derive(Component, Clone, Copy, Debug, Default, Reflect)] -#[reflect(Component)] +#[reflect(Component, Default)] pub struct Frustum { #[reflect(ignore)] pub half_spaces: [HalfSpace; 6], @@ -303,7 +303,7 @@ impl Frustum { } #[derive(Component, Clone, Debug, Default, Reflect)] -#[reflect(Component)] +#[reflect(Component, Default)] pub struct CubemapFrusta { #[reflect(ignore)] pub frusta: [Frustum; 6], @@ -319,7 +319,7 @@ impl CubemapFrusta { } #[derive(Component, Debug, Default, Reflect)] -#[reflect(Component)] +#[reflect(Component, Default)] pub struct CascadesFrusta { #[reflect(ignore)] pub frusta: EntityHashMap>, diff --git a/crates/bevy_render/src/render_resource/shader.rs b/crates/bevy_render/src/render_resource/shader.rs index 677378cc90f16d..49d61533eaeebe 100644 --- a/crates/bevy_render/src/render_resource/shader.rs +++ b/crates/bevy_render/src/render_resource/shader.rs @@ -2,7 +2,7 @@ use super::ShaderDefVal; use crate::define_atomic_id; use bevy_asset::{io::Reader, Asset, AssetLoader, AssetPath, Handle, LoadContext}; use bevy_reflect::TypePath; -use bevy_utils::{tracing::error, BoxedFuture}; +use bevy_utils::tracing::error; use futures_lite::AsyncReadExt; use std::{borrow::Cow, marker::Copy}; use thiserror::Error; @@ -259,43 +259,39 @@ impl AssetLoader for ShaderLoader { type Asset = Shader; type Settings = (); type Error = ShaderLoaderError; - fn load<'a>( + async fn load<'a>( &'a self, - reader: &'a mut Reader, + reader: &'a mut Reader<'_>, _settings: &'a Self::Settings, - load_context: &'a mut LoadContext, - ) -> BoxedFuture<'a, Result> { - Box::pin(async move { - let ext = load_context.path().extension().unwrap().to_str().unwrap(); - let path = load_context.asset_path().to_string(); - // On windows, the path will inconsistently use \ or /. - // TODO: remove this once AssetPath forces cross-platform "slash" consistency. See #10511 - let path = path.replace(std::path::MAIN_SEPARATOR, "/"); - let mut bytes = Vec::new(); - reader.read_to_end(&mut bytes).await?; - let mut shader = match ext { - "spv" => Shader::from_spirv(bytes, load_context.path().to_string_lossy()), - "wgsl" => Shader::from_wgsl(String::from_utf8(bytes)?, path), - "vert" => { - Shader::from_glsl(String::from_utf8(bytes)?, naga::ShaderStage::Vertex, path) - } - "frag" => { - Shader::from_glsl(String::from_utf8(bytes)?, naga::ShaderStage::Fragment, path) - } - "comp" => { - Shader::from_glsl(String::from_utf8(bytes)?, naga::ShaderStage::Compute, path) - } - _ => panic!("unhandled extension: {ext}"), - }; + load_context: &'a mut LoadContext<'_>, + ) -> Result { + let ext = load_context.path().extension().unwrap().to_str().unwrap(); + let path = load_context.asset_path().to_string(); + // On windows, the path will inconsistently use \ or /. + // TODO: remove this once AssetPath forces cross-platform "slash" consistency. See #10511 + let path = path.replace(std::path::MAIN_SEPARATOR, "/"); + let mut bytes = Vec::new(); + reader.read_to_end(&mut bytes).await?; + let mut shader = match ext { + "spv" => Shader::from_spirv(bytes, load_context.path().to_string_lossy()), + "wgsl" => Shader::from_wgsl(String::from_utf8(bytes)?, path), + "vert" => Shader::from_glsl(String::from_utf8(bytes)?, naga::ShaderStage::Vertex, path), + "frag" => { + Shader::from_glsl(String::from_utf8(bytes)?, naga::ShaderStage::Fragment, path) + } + "comp" => { + Shader::from_glsl(String::from_utf8(bytes)?, naga::ShaderStage::Compute, path) + } + _ => panic!("unhandled extension: {ext}"), + }; - // collect and store file dependencies - for import in &shader.imports { - if let ShaderImport::AssetPath(asset_path) = import { - shader.file_dependencies.push(load_context.load(asset_path)); - } + // collect and store file dependencies + for import in &shader.imports { + if let ShaderImport::AssetPath(asset_path) = import { + shader.file_dependencies.push(load_context.load(asset_path)); } - Ok(shader) - }) + } + Ok(shader) } fn extensions(&self) -> &[&str] { diff --git a/crates/bevy_render/src/texture/compressed_image_saver.rs b/crates/bevy_render/src/texture/compressed_image_saver.rs index dde2a900b4a21c..0ab053df331f6f 100644 --- a/crates/bevy_render/src/texture/compressed_image_saver.rs +++ b/crates/bevy_render/src/texture/compressed_image_saver.rs @@ -1,6 +1,6 @@ use crate::texture::{Image, ImageFormat, ImageFormatSetting, ImageLoader, ImageLoaderSettings}; use bevy_asset::saver::{AssetSaver, SavedAsset}; -use futures_lite::{AsyncWriteExt, FutureExt}; +use futures_lite::AsyncWriteExt; use thiserror::Error; pub struct CompressedImageSaver; @@ -19,46 +19,46 @@ impl AssetSaver for CompressedImageSaver { type OutputLoader = ImageLoader; type Error = CompressedImageSaverError; - fn save<'a>( + async fn save<'a>( &'a self, writer: &'a mut bevy_asset::io::Writer, image: SavedAsset<'a, Self::Asset>, _settings: &'a Self::Settings, - ) -> bevy_utils::BoxedFuture<'a, Result> { - // PERF: this should live inside the future, but CompressorParams and Compressor are not Send / can't be owned by the BoxedFuture (which _is_ Send) - let mut compressor_params = basis_universal::CompressorParams::new(); - compressor_params.set_basis_format(basis_universal::BasisTextureFormat::UASTC4x4); - compressor_params.set_generate_mipmaps(true); + ) -> Result { let is_srgb = image.texture_descriptor.format.is_srgb(); - let color_space = if is_srgb { - basis_universal::ColorSpace::Srgb - } else { - basis_universal::ColorSpace::Linear - }; - compressor_params.set_color_space(color_space); - compressor_params.set_uastc_quality_level(basis_universal::UASTC_QUALITY_DEFAULT); - let mut source_image = compressor_params.source_image_mut(0); - let size = image.size(); - source_image.init(&image.data, size.x, size.y, 4); + let compressed_basis_data = { + let mut compressor_params = basis_universal::CompressorParams::new(); + compressor_params.set_basis_format(basis_universal::BasisTextureFormat::UASTC4x4); + compressor_params.set_generate_mipmaps(true); + let color_space = if is_srgb { + basis_universal::ColorSpace::Srgb + } else { + basis_universal::ColorSpace::Linear + }; + compressor_params.set_color_space(color_space); + compressor_params.set_uastc_quality_level(basis_universal::UASTC_QUALITY_DEFAULT); + + let mut source_image = compressor_params.source_image_mut(0); + let size = image.size(); + source_image.init(&image.data, size.x, size.y, 4); + + let mut compressor = basis_universal::Compressor::new(4); + // SAFETY: the CompressorParams are "valid" to the best of our knowledge. The basis-universal + // library bindings note that invalid params might produce undefined behavior. + unsafe { + compressor.init(&compressor_params); + compressor.process().unwrap(); + } + compressor.basis_file().to_vec() + }; - let mut compressor = basis_universal::Compressor::new(4); - // SAFETY: the CompressorParams are "valid" to the best of our knowledge. The basis-universal - // library bindings note that invalid params might produce undefined behavior. - unsafe { - compressor.init(&compressor_params); - compressor.process().unwrap(); - } - let compressed_basis_data = compressor.basis_file().to_vec(); - async move { - writer.write_all(&compressed_basis_data).await?; - Ok(ImageLoaderSettings { - format: ImageFormatSetting::Format(ImageFormat::Basis), - is_srgb, - sampler: image.sampler.clone(), - asset_usage: image.asset_usage, - }) - } - .boxed() + writer.write_all(&compressed_basis_data).await?; + Ok(ImageLoaderSettings { + format: ImageFormatSetting::Format(ImageFormat::Basis), + is_srgb, + sampler: image.sampler.clone(), + asset_usage: image.asset_usage, + }) } } diff --git a/crates/bevy_render/src/texture/exr_texture_loader.rs b/crates/bevy_render/src/texture/exr_texture_loader.rs index 9f0d6706580482..d5f39aa3c05426 100644 --- a/crates/bevy_render/src/texture/exr_texture_loader.rs +++ b/crates/bevy_render/src/texture/exr_texture_loader.rs @@ -6,7 +6,6 @@ use bevy_asset::{ io::{AsyncReadExt, Reader}, AssetLoader, LoadContext, }; -use bevy_utils::BoxedFuture; use image::ImageDecoder; use serde::{Deserialize, Serialize}; use thiserror::Error; @@ -36,45 +35,43 @@ impl AssetLoader for ExrTextureLoader { type Settings = ExrTextureLoaderSettings; type Error = ExrTextureLoaderError; - fn load<'a>( + async fn load<'a>( &'a self, - reader: &'a mut Reader, + reader: &'a mut Reader<'_>, settings: &'a Self::Settings, - _load_context: &'a mut LoadContext, - ) -> BoxedFuture<'a, Result> { - Box::pin(async move { - let format = TextureFormat::Rgba32Float; - debug_assert_eq!( - format.pixel_size(), - 4 * 4, - "Format should have 32bit x 4 size" - ); + _load_context: &'a mut LoadContext<'_>, + ) -> Result { + let format = TextureFormat::Rgba32Float; + debug_assert_eq!( + format.pixel_size(), + 4 * 4, + "Format should have 32bit x 4 size" + ); - let mut bytes = Vec::new(); - reader.read_to_end(&mut bytes).await?; - let decoder = image::codecs::openexr::OpenExrDecoder::with_alpha_preference( - std::io::Cursor::new(bytes), - Some(true), - )?; - let (width, height) = decoder.dimensions(); + let mut bytes = Vec::new(); + reader.read_to_end(&mut bytes).await?; + let decoder = image::codecs::openexr::OpenExrDecoder::with_alpha_preference( + std::io::Cursor::new(bytes), + Some(true), + )?; + let (width, height) = decoder.dimensions(); - let total_bytes = decoder.total_bytes() as usize; + let total_bytes = decoder.total_bytes() as usize; - let mut buf = vec![0u8; total_bytes]; - decoder.read_image(buf.as_mut_slice())?; + let mut buf = vec![0u8; total_bytes]; + decoder.read_image(buf.as_mut_slice())?; - Ok(Image::new( - Extent3d { - width, - height, - depth_or_array_layers: 1, - }, - TextureDimension::D2, - buf, - format, - settings.asset_usage, - )) - }) + Ok(Image::new( + Extent3d { + width, + height, + depth_or_array_layers: 1, + }, + TextureDimension::D2, + buf, + format, + settings.asset_usage, + )) } fn extensions(&self) -> &[&str] { diff --git a/crates/bevy_render/src/texture/hdr_texture_loader.rs b/crates/bevy_render/src/texture/hdr_texture_loader.rs index 1fd021863943c3..641055690fd58b 100644 --- a/crates/bevy_render/src/texture/hdr_texture_loader.rs +++ b/crates/bevy_render/src/texture/hdr_texture_loader.rs @@ -29,48 +29,46 @@ impl AssetLoader for HdrTextureLoader { type Asset = Image; type Settings = HdrTextureLoaderSettings; type Error = HdrTextureLoaderError; - fn load<'a>( + async fn load<'a>( &'a self, - reader: &'a mut Reader, + reader: &'a mut Reader<'_>, settings: &'a Self::Settings, - _load_context: &'a mut LoadContext, - ) -> bevy_utils::BoxedFuture<'a, Result> { - Box::pin(async move { - let format = TextureFormat::Rgba32Float; - debug_assert_eq!( - format.pixel_size(), - 4 * 4, - "Format should have 32bit x 4 size" - ); + _load_context: &'a mut LoadContext<'_>, + ) -> Result { + let format = TextureFormat::Rgba32Float; + debug_assert_eq!( + format.pixel_size(), + 4 * 4, + "Format should have 32bit x 4 size" + ); - let mut bytes = Vec::new(); - reader.read_to_end(&mut bytes).await?; - let decoder = image::codecs::hdr::HdrDecoder::new(bytes.as_slice())?; - let info = decoder.metadata(); - let rgb_data = decoder.read_image_hdr()?; - let mut rgba_data = Vec::with_capacity(rgb_data.len() * format.pixel_size()); + let mut bytes = Vec::new(); + reader.read_to_end(&mut bytes).await?; + let decoder = image::codecs::hdr::HdrDecoder::new(bytes.as_slice())?; + let info = decoder.metadata(); + let rgb_data = decoder.read_image_hdr()?; + let mut rgba_data = Vec::with_capacity(rgb_data.len() * format.pixel_size()); - for rgb in rgb_data { - let alpha = 1.0f32; + for rgb in rgb_data { + let alpha = 1.0f32; - rgba_data.extend_from_slice(&rgb.0[0].to_ne_bytes()); - rgba_data.extend_from_slice(&rgb.0[1].to_ne_bytes()); - rgba_data.extend_from_slice(&rgb.0[2].to_ne_bytes()); - rgba_data.extend_from_slice(&alpha.to_ne_bytes()); - } + rgba_data.extend_from_slice(&rgb.0[0].to_ne_bytes()); + rgba_data.extend_from_slice(&rgb.0[1].to_ne_bytes()); + rgba_data.extend_from_slice(&rgb.0[2].to_ne_bytes()); + rgba_data.extend_from_slice(&alpha.to_ne_bytes()); + } - Ok(Image::new( - Extent3d { - width: info.width, - height: info.height, - depth_or_array_layers: 1, - }, - TextureDimension::D2, - rgba_data, - format, - settings.asset_usage, - )) - }) + Ok(Image::new( + Extent3d { + width: info.width, + height: info.height, + depth_or_array_layers: 1, + }, + TextureDimension::D2, + rgba_data, + format, + settings.asset_usage, + )) } fn extensions(&self) -> &[&str] { diff --git a/crates/bevy_render/src/texture/image.rs b/crates/bevy_render/src/texture/image.rs index b052519b056f67..d163841837c039 100644 --- a/crates/bevy_render/src/texture/image.rs +++ b/crates/bevy_render/src/texture/image.rs @@ -15,7 +15,7 @@ use bevy_asset::Asset; use bevy_derive::{Deref, DerefMut}; use bevy_ecs::system::{lifetimeless::SRes, Resource, SystemParamItem}; use bevy_math::{AspectRatio, UVec2, Vec2}; -use bevy_reflect::Reflect; +use bevy_reflect::prelude::*; use serde::{Deserialize, Serialize}; use std::hash::Hash; use thiserror::Error; @@ -112,7 +112,7 @@ impl ImageFormat { } #[derive(Asset, Reflect, Debug, Clone)] -#[reflect_value] +#[reflect_value(Default)] pub struct Image { pub data: Vec, // TODO: this nesting makes accessing Image metadata verbose. Either flatten out descriptor or add accessors diff --git a/crates/bevy_render/src/texture/image_loader.rs b/crates/bevy_render/src/texture/image_loader.rs index 44a4fdb9251cf0..534d064409b3ff 100644 --- a/crates/bevy_render/src/texture/image_loader.rs +++ b/crates/bevy_render/src/texture/image_loader.rs @@ -85,37 +85,35 @@ impl AssetLoader for ImageLoader { type Asset = Image; type Settings = ImageLoaderSettings; type Error = ImageLoaderError; - fn load<'a>( + async fn load<'a>( &'a self, - reader: &'a mut Reader, + reader: &'a mut Reader<'_>, settings: &'a ImageLoaderSettings, - load_context: &'a mut LoadContext, - ) -> bevy_utils::BoxedFuture<'a, Result> { - Box::pin(async move { - // use the file extension for the image type - let ext = load_context.path().extension().unwrap().to_str().unwrap(); + load_context: &'a mut LoadContext<'_>, + ) -> Result { + // use the file extension for the image type + let ext = load_context.path().extension().unwrap().to_str().unwrap(); - let mut bytes = Vec::new(); - reader.read_to_end(&mut bytes).await?; - let image_type = match settings.format { - ImageFormatSetting::FromExtension => ImageType::Extension(ext), - ImageFormatSetting::Format(format) => ImageType::Format(format), - }; - Ok(Image::from_buffer( - #[cfg(all(debug_assertions, feature = "dds"))] - load_context.path().display().to_string(), - &bytes, - image_type, - self.supported_compressed_formats, - settings.is_srgb, - settings.sampler.clone(), - settings.asset_usage, - ) - .map_err(|err| FileTextureError { - error: err, - path: format!("{}", load_context.path().display()), - })?) - }) + let mut bytes = Vec::new(); + reader.read_to_end(&mut bytes).await?; + let image_type = match settings.format { + ImageFormatSetting::FromExtension => ImageType::Extension(ext), + ImageFormatSetting::Format(format) => ImageType::Format(format), + }; + Ok(Image::from_buffer( + #[cfg(all(debug_assertions, feature = "dds"))] + load_context.path().display().to_string(), + &bytes, + image_type, + self.supported_compressed_formats, + settings.is_srgb, + settings.sampler.clone(), + settings.asset_usage, + ) + .map_err(|err| FileTextureError { + error: err, + path: format!("{}", load_context.path().display()), + })?) } fn extensions(&self) -> &[&str] { diff --git a/crates/bevy_render/src/view/mod.rs b/crates/bevy_render/src/view/mod.rs index 444888b569ad0a..3b888e33e69ddf 100644 --- a/crates/bevy_render/src/view/mod.rs +++ b/crates/bevy_render/src/view/mod.rs @@ -90,7 +90,7 @@ impl Plugin for ViewPlugin { #[derive( Resource, Default, Clone, Copy, ExtractResource, Reflect, PartialEq, PartialOrd, Debug, )] -#[reflect(Resource)] +#[reflect(Resource, Default)] pub enum Msaa { Off = 1, Sample2 = 2, diff --git a/crates/bevy_render/src/view/visibility/mod.rs b/crates/bevy_render/src/view/visibility/mod.rs index c674a36829e05b..50ea668f14c7cc 100644 --- a/crates/bevy_render/src/view/visibility/mod.rs +++ b/crates/bevy_render/src/view/visibility/mod.rs @@ -168,7 +168,7 @@ pub struct NoFrustumCulling; /// This component is intended to be attached to the same entity as the [`Camera`] and /// the [`Frustum`] defining the view. #[derive(Clone, Component, Default, Debug, Reflect)] -#[reflect(Component)] +#[reflect(Component, Default)] pub struct VisibleEntities { #[reflect(ignore)] pub entities: Vec, diff --git a/crates/bevy_scene/src/scene_loader.rs b/crates/bevy_scene/src/scene_loader.rs index f4dce7c66a3d88..107d014b2bb092 100644 --- a/crates/bevy_scene/src/scene_loader.rs +++ b/crates/bevy_scene/src/scene_loader.rs @@ -6,7 +6,6 @@ use bevy_asset::{io::Reader, AssetLoader, AsyncReadExt, LoadContext}; use bevy_ecs::reflect::AppTypeRegistry; use bevy_ecs::world::{FromWorld, World}; use bevy_reflect::TypeRegistryArc; -use bevy_utils::BoxedFuture; #[cfg(feature = "serialize")] use serde::de::DeserializeSeed; use thiserror::Error; @@ -44,23 +43,21 @@ impl AssetLoader for SceneLoader { type Settings = (); type Error = SceneLoaderError; - fn load<'a>( + async fn load<'a>( &'a self, - reader: &'a mut Reader, + reader: &'a mut Reader<'_>, _settings: &'a (), - _load_context: &'a mut LoadContext, - ) -> BoxedFuture<'a, Result> { - Box::pin(async move { - let mut bytes = Vec::new(); - reader.read_to_end(&mut bytes).await?; - let mut deserializer = ron::de::Deserializer::from_bytes(&bytes)?; - let scene_deserializer = SceneDeserializer { - type_registry: &self.type_registry.read(), - }; - Ok(scene_deserializer - .deserialize(&mut deserializer) - .map_err(|e| deserializer.span_error(e))?) - }) + _load_context: &'a mut LoadContext<'_>, + ) -> Result { + let mut bytes = Vec::new(); + reader.read_to_end(&mut bytes).await?; + let mut deserializer = ron::de::Deserializer::from_bytes(&bytes)?; + let scene_deserializer = SceneDeserializer { + type_registry: &self.type_registry.read(), + }; + Ok(scene_deserializer + .deserialize(&mut deserializer) + .map_err(|e| deserializer.span_error(e))?) } fn extensions(&self) -> &[&str] { diff --git a/crates/bevy_text/src/font_loader.rs b/crates/bevy_text/src/font_loader.rs index a47abbd9619a05..45f3e9701e11be 100644 --- a/crates/bevy_text/src/font_loader.rs +++ b/crates/bevy_text/src/font_loader.rs @@ -21,17 +21,15 @@ impl AssetLoader for FontLoader { type Asset = Font; type Settings = (); type Error = FontLoaderError; - fn load<'a>( + async fn load<'a>( &'a self, - reader: &'a mut Reader, + reader: &'a mut Reader<'_>, _settings: &'a (), - _load_context: &'a mut LoadContext, - ) -> bevy_utils::BoxedFuture<'a, Result> { - Box::pin(async move { - let mut bytes = Vec::new(); - reader.read_to_end(&mut bytes).await?; - Ok(Font::try_from_bytes(bytes)?) - }) + _load_context: &'a mut LoadContext<'_>, + ) -> Result { + let mut bytes = Vec::new(); + reader.read_to_end(&mut bytes).await?; + Ok(Font::try_from_bytes(bytes)?) } fn extensions(&self) -> &[&str] { diff --git a/crates/bevy_ui/src/lib.rs b/crates/bevy_ui/src/lib.rs index ade10424ff9953..2ec6b130350c61 100644 --- a/crates/bevy_ui/src/lib.rs +++ b/crates/bevy_ui/src/lib.rs @@ -112,6 +112,7 @@ impl Plugin for UiPlugin { .register_type::() .register_type::() .register_type::() + .register_type::() .register_type::() .register_type::() .register_type::() diff --git a/crates/bevy_ui/src/node_bundles.rs b/crates/bevy_ui/src/node_bundles.rs index 57b2bb4857d220..17dddacc872536 100644 --- a/crates/bevy_ui/src/node_bundles.rs +++ b/crates/bevy_ui/src/node_bundles.rs @@ -6,8 +6,8 @@ use crate::widget::TextFlags; use crate::{ widget::{Button, UiImageSize}, - BackgroundColor, BorderColor, ContentSize, FocusPolicy, Interaction, Node, Style, UiImage, - UiMaterial, ZIndex, + BackgroundColor, BorderColor, BorderRadius, ContentSize, FocusPolicy, Interaction, Node, Style, + UiImage, UiMaterial, ZIndex, }; use bevy_asset::Handle; use bevy_color::Color; @@ -34,6 +34,8 @@ pub struct NodeBundle { pub background_color: BackgroundColor, /// The color of the Node's border pub border_color: BorderColor, + /// The border radius of the node + pub border_radius: BorderRadius, /// Whether this node should block interaction with lower nodes pub focus_policy: FocusPolicy, /// The transform of the node @@ -62,6 +64,7 @@ impl Default for NodeBundle { // Transparent background background_color: Color::NONE.into(), border_color: Color::NONE.into(), + border_radius: BorderRadius::default(), node: Default::default(), style: Default::default(), focus_policy: Default::default(), @@ -314,6 +317,8 @@ pub struct ButtonBundle { pub focus_policy: FocusPolicy, /// The color of the Node's border pub border_color: BorderColor, + /// The border radius of the node + pub border_radius: BorderRadius, /// The image of the node pub image: UiImage, /// The transform of the node @@ -344,6 +349,7 @@ impl Default for ButtonBundle { interaction: Default::default(), focus_policy: FocusPolicy::Block, border_color: BorderColor(Color::NONE), + border_radius: BorderRadius::default(), image: Default::default(), transform: Default::default(), global_transform: Default::default(), diff --git a/crates/bevy_ui/src/render/mod.rs b/crates/bevy_ui/src/render/mod.rs index fcec109409d59d..882a87ba484b55 100644 --- a/crates/bevy_ui/src/render/mod.rs +++ b/crates/bevy_ui/src/render/mod.rs @@ -9,21 +9,23 @@ use bevy_core_pipeline::{core_2d::Camera2d, core_3d::Camera3d}; use bevy_hierarchy::Parent; use bevy_render::{render_phase::PhaseItem, view::ViewVisibility, ExtractSchedule, Render}; use bevy_sprite::{SpriteAssetEvents, TextureAtlas}; +use bevy_window::{PrimaryWindow, Window}; pub use pipeline::*; pub use render_pass::*; pub use ui_material_pipeline::*; use crate::graph::{NodeUi, SubGraphUi}; use crate::{ - texture_slice::ComputedTextureSlices, BackgroundColor, BorderColor, CalculatedClip, - ContentSize, DefaultUiCamera, Node, Outline, Style, TargetCamera, UiImage, UiScale, Val, + texture_slice::ComputedTextureSlices, BackgroundColor, BorderColor, BorderRadius, + CalculatedClip, ContentSize, DefaultUiCamera, Node, Outline, Style, TargetCamera, UiImage, + UiScale, Val, }; use bevy_app::prelude::*; use bevy_asset::{load_internal_asset, AssetEvent, AssetId, Assets, Handle}; use bevy_ecs::entity::EntityHashMap; use bevy_ecs::prelude::*; -use bevy_math::{Mat4, Rect, URect, UVec4, Vec2, Vec3, Vec4Swizzles}; +use bevy_math::{Mat4, Rect, URect, UVec4, Vec2, Vec3, Vec3Swizzles, Vec4, Vec4Swizzles}; use bevy_render::{ camera::Camera, render_asset::RenderAssets, @@ -141,6 +143,14 @@ fn get_ui_graph(render_app: &mut App) -> RenderGraph { ui_graph } +/// The type of UI node. +/// This is used to determine how to render the UI node. +#[derive(Clone, Copy, Debug, PartialEq)] +pub enum NodeType { + Rect, + Border, +} + pub struct ExtractedUiNode { pub stack_index: u32, pub transform: Mat4, @@ -155,6 +165,13 @@ pub struct ExtractedUiNode { // it is defaulted to a single camera if only one exists. // Nodes with ambiguous camera will be ignored. pub camera_entity: Entity, + /// Border radius of the UI node. + /// Ordering: top left, top right, bottom right, bottom left. + pub border_radius: [f32; 4], + /// Border thickness of the UI node. + /// Ordering: left, top, right, bottom. + pub border: [f32; 4], + pub node_type: NodeType, } #[derive(Resource, Default)] @@ -164,7 +181,9 @@ pub struct ExtractedUiNodes { pub fn extract_uinode_background_colors( mut extracted_uinodes: ResMut, + windows: Extract>>, default_ui_camera: Extract, + ui_scale: Extract>, uinode_query: Extract< Query<( Entity, @@ -174,11 +193,26 @@ pub fn extract_uinode_background_colors( Option<&CalculatedClip>, Option<&TargetCamera>, &BackgroundColor, + Option<&BorderRadius>, )>, >, ) { - for (entity, uinode, transform, view_visibility, clip, camera, background_color) in - &uinode_query + let viewport_size = windows + .get_single() + .map(|window| window.resolution.size()) + .unwrap_or(Vec2::ZERO) + * ui_scale.0; + + for ( + entity, + uinode, + transform, + view_visibility, + clip, + camera, + background_color, + border_radius, + ) in &uinode_query { let Some(camera_entity) = camera.map(TargetCamera::entity).or(default_ui_camera.get()) else { @@ -190,6 +224,12 @@ pub fn extract_uinode_background_colors( continue; } + let border_radius = if let Some(border_radius) = border_radius { + resolve_border_radius(border_radius, uinode.size(), viewport_size, ui_scale.0) + } else { + [0.; 4] + }; + extracted_uinodes.uinodes.insert( entity, ExtractedUiNode { @@ -206,6 +246,9 @@ pub fn extract_uinode_background_colors( flip_x: false, flip_y: false, camera_entity, + border: [0.; 4], + border_radius, + node_type: NodeType::Rect, }, ); } @@ -214,7 +257,9 @@ pub fn extract_uinode_background_colors( pub fn extract_uinode_images( mut commands: Commands, mut extracted_uinodes: ResMut, + windows: Extract>>, texture_atlases: Extract>>, + ui_scale: Extract>, default_ui_camera: Extract, uinode_query: Extract< Query<( @@ -226,10 +271,19 @@ pub fn extract_uinode_images( &UiImage, Option<&TextureAtlas>, Option<&ComputedTextureSlices>, + Option<&BorderRadius>, )>, >, ) { - for (uinode, transform, view_visibility, clip, camera, image, atlas, slices) in &uinode_query { + let viewport_size = windows + .get_single() + .map(|window| window.resolution.size()) + .unwrap_or(Vec2::ZERO) + * ui_scale.0; + + for (uinode, transform, view_visibility, clip, camera, image, atlas, slices, border_radius) in + &uinode_query + { let Some(camera_entity) = camera.map(TargetCamera::entity).or(default_ui_camera.get()) else { continue; @@ -272,6 +326,12 @@ pub fn extract_uinode_images( ), }; + let border_radius = if let Some(border_radius) = border_radius { + resolve_border_radius(border_radius, uinode.size(), viewport_size, ui_scale.0) + } else { + [0.; 4] + }; + extracted_uinodes.uinodes.insert( commands.spawn_empty().id(), ExtractedUiNode { @@ -285,6 +345,9 @@ pub fn extract_uinode_images( flip_x: image.flip_x, flip_y: image.flip_y, camera_entity, + border: [0.; 4], + border_radius, + node_type: NodeType::Rect, }, ); } @@ -302,6 +365,55 @@ pub(crate) fn resolve_border_thickness(value: Val, parent_width: f32, viewport_s } } +pub(crate) fn resolve_border_radius( + &values: &BorderRadius, + node_size: Vec2, + viewport_size: Vec2, + ui_scale: f32, +) -> [f32; 4] { + let max_radius = 0.5 * node_size.min_element() * ui_scale; + [ + values.top_left, + values.top_right, + values.bottom_right, + values.bottom_left, + ] + .map(|value| { + match value { + Val::Auto => 0., + Val::Px(px) => ui_scale * px, + Val::Percent(percent) => node_size.min_element() * percent / 100., + Val::Vw(percent) => viewport_size.x * percent / 100., + Val::Vh(percent) => viewport_size.y * percent / 100., + Val::VMin(percent) => viewport_size.min_element() * percent / 100., + Val::VMax(percent) => viewport_size.max_element() * percent / 100., + } + .clamp(0., max_radius) + }) +} + +#[inline] +fn clamp_corner(r: f32, size: Vec2, offset: Vec2) -> f32 { + let s = 0.5 * size + offset; + let sm = s.x.min(s.y); + r.min(sm) +} + +#[inline] +fn clamp_radius( + [top_left, top_right, bottom_right, bottom_left]: [f32; 4], + size: Vec2, + border: Vec4, +) -> [f32; 4] { + let s = size - border.xy() - border.zw(); + [ + clamp_corner(top_left, s, border.xy()), + clamp_corner(top_right, s, border.zy()), + clamp_corner(bottom_right, s, border.zw()), + clamp_corner(bottom_left, s, border.xw()), + ] +} + pub fn extract_uinode_borders( mut commands: Commands, mut extracted_uinodes: ResMut, @@ -319,6 +431,7 @@ pub fn extract_uinode_borders( Option<&Parent>, &Style, &BorderColor, + &BorderRadius, ), Without, >, @@ -327,8 +440,17 @@ pub fn extract_uinode_borders( ) { let image = AssetId::::default(); - for (node, global_transform, view_visibility, clip, camera, parent, style, border_color) in - &uinode_query + for ( + node, + global_transform, + view_visibility, + clip, + camera, + parent, + style, + border_color, + border_radius, + ) in &uinode_query { let Some(camera_entity) = camera.map(TargetCamera::entity).or(default_ui_camera.get()) else { @@ -368,60 +490,40 @@ pub fn extract_uinode_borders( let bottom = resolve_border_thickness(style.border.bottom, parent_width, ui_logical_viewport_size); - // Calculate the border rects, ensuring no overlap. - // The border occupies the space between the node's bounding rect and the node's bounding rect inset in each direction by the node's corresponding border value. - let max = 0.5 * node.size(); - let min = -max; - let inner_min = min + Vec2::new(left, top); - let inner_max = (max - Vec2::new(right, bottom)).max(inner_min); - let border_rects = [ - // Left border - Rect { - min, - max: Vec2::new(inner_min.x, max.y), - }, - // Right border - Rect { - min: Vec2::new(inner_max.x, min.y), - max, - }, - // Top border - Rect { - min: Vec2::new(inner_min.x, min.y), - max: Vec2::new(inner_max.x, inner_min.y), - }, - // Bottom border - Rect { - min: Vec2::new(inner_min.x, inner_max.y), - max: Vec2::new(inner_max.x, max.y), - }, - ]; + let border = [left, top, right, bottom]; + let border_radius = resolve_border_radius( + border_radius, + node.size(), + ui_logical_viewport_size, + ui_scale.0, + ); + + let border_radius = clamp_radius(border_radius, node.size(), border.into()); let transform = global_transform.compute_matrix(); - for edge in border_rects { - if edge.min.x < edge.max.x && edge.min.y < edge.max.y { - extracted_uinodes.uinodes.insert( - commands.spawn_empty().id(), - ExtractedUiNode { - stack_index: node.stack_index, - // This translates the uinode's transform to the center of the current border rectangle - transform: transform * Mat4::from_translation(edge.center().extend(0.)), - color: border_color.0.into(), - rect: Rect { - max: edge.size(), - ..Default::default() - }, - image, - atlas_size: None, - clip: clip.map(|clip| clip.clip), - flip_x: false, - flip_y: false, - camera_entity, - }, - ); - } - } + extracted_uinodes.uinodes.insert( + commands.spawn_empty().id(), + ExtractedUiNode { + stack_index: node.stack_index, + // This translates the uinode's transform to the center of the current border rectangle + transform, + color: border_color.0.into(), + rect: Rect { + max: node.size(), + ..Default::default() + }, + image, + atlas_size: None, + clip: clip.map(|clip| clip.clip), + flip_x: false, + flip_y: false, + camera_entity, + border_radius, + border, + node_type: NodeType::Border, + }, + ); } } @@ -490,7 +592,6 @@ pub fn extract_uinode_outlines( ]; let transform = global_transform.compute_matrix(); - for edge in outline_edges { if edge.min.x < edge.max.x && edge.min.y < edge.max.y { extracted_uinodes.uinodes.insert( @@ -510,6 +611,9 @@ pub fn extract_uinode_outlines( flip_x: false, flip_y: false, camera_entity, + border: [0.; 4], + border_radius: [0.; 4], + node_type: NodeType::Rect, }, ); } @@ -680,6 +784,9 @@ pub fn extract_uinode_text( flip_x: false, flip_y: false, camera_entity, + border: [0.; 4], + border_radius: [0.; 4], + node_type: NodeType::Rect, }, ); } @@ -692,12 +799,23 @@ struct UiVertex { pub position: [f32; 3], pub uv: [f32; 2], pub color: [f32; 4], - pub mode: u32, + /// Shader flags to determine how to render the UI node. + /// See [`shader_flags`] for possible values. + pub flags: u32, + /// Border radius of the UI node. + /// Ordering: top left, top right, bottom right, bottom left. + pub radius: [f32; 4], + /// Border thickness of the UI node. + /// Ordering: left, top, right, bottom. + pub border: [f32; 4], + /// Size of the UI node. + pub size: [f32; 2], } #[derive(Resource)] pub struct UiMeta { vertices: BufferVec, + indices: BufferVec, view_bind_group: Option, } @@ -705,6 +823,7 @@ impl Default for UiMeta { fn default() -> Self { Self { vertices: BufferVec::new(BufferUsages::VERTEX), + indices: BufferVec::new(BufferUsages::INDEX), view_bind_group: None, } } @@ -726,8 +845,14 @@ pub struct UiBatch { pub camera: Entity, } -const TEXTURED_QUAD: u32 = 0; -const UNTEXTURED_QUAD: u32 = 1; +/// The values here should match the values for the constants in `ui.wgsl` +pub mod shader_flags { + pub const UNTEXTURED: u32 = 0; + pub const TEXTURED: u32 = 1; + /// Ordering: top left, top right, bottom right, bottom left. + pub const CORNERS: [u32; 4] = [0, 2, 2 | 4, 4]; + pub const BORDER: u32 = 8; +} #[allow(clippy::too_many_arguments)] pub fn queue_uinodes( @@ -802,14 +927,17 @@ pub fn prepare_uinodes( let mut batches: Vec<(Entity, UiBatch)> = Vec::with_capacity(*previous_len); ui_meta.vertices.clear(); + ui_meta.indices.clear(); ui_meta.view_bind_group = Some(render_device.create_bind_group( "ui_view_bind_group", &ui_pipeline.view_layout, &BindGroupEntries::single(view_binding), )); - // Vertex buffer index - let mut index = 0; + // Buffer indexes + let mut vertices_index = 0; + let mut indices_index = 0; + for mut ui_phase in &mut phases { let mut batch_item_index = 0; let mut batch_image_handle = AssetId::invalid(); @@ -832,7 +960,7 @@ pub fn prepare_uinodes( batch_image_handle = extracted_uinode.image; let new_batch = UiBatch { - range: index..index, + range: vertices_index..vertices_index, image: extracted_uinode.image, camera: extracted_uinode.camera_entity, }; @@ -882,10 +1010,10 @@ pub fn prepare_uinodes( } } - let mode = if extracted_uinode.image != AssetId::default() { - TEXTURED_QUAD + let mut flags = if extracted_uinode.image != AssetId::default() { + shader_flags::TEXTURED } else { - UNTEXTURED_QUAD + shader_flags::UNTEXTURED }; let mut uinode_rect = extracted_uinode.rect; @@ -946,7 +1074,7 @@ pub fn prepare_uinodes( continue; } } - let uvs = if mode == UNTEXTURED_QUAD { + let uvs = if flags == shader_flags::UNTEXTURED { [Vec2::ZERO, Vec2::X, Vec2::ONE, Vec2::Y] } else { let atlas_extent = extracted_uinode.atlas_size.unwrap_or(uinode_rect.max); @@ -986,16 +1114,30 @@ pub fn prepare_uinodes( }; let color = extracted_uinode.color.to_f32_array(); - for i in QUAD_INDICES { + if extracted_uinode.node_type == NodeType::Border { + flags |= shader_flags::BORDER; + } + + for i in 0..4 { ui_meta.vertices.push(UiVertex { position: positions_clipped[i].into(), uv: uvs[i].into(), color, - mode, + flags: flags | shader_flags::CORNERS[i], + radius: extracted_uinode.border_radius, + border: extracted_uinode.border, + size: transformed_rect_size.xy().into(), }); } - index += QUAD_INDICES.len() as u32; - existing_batch.unwrap().1.range.end = index; + + for &i in &QUAD_INDICES { + ui_meta.indices.push(indices_index + i as u32); + } + + vertices_index += 6; + indices_index += 4; + + existing_batch.unwrap().1.range.end = vertices_index; ui_phase.items[batch_item_index].batch_range_mut().end += 1; } else { batch_image_handle = AssetId::invalid(); @@ -1003,6 +1145,7 @@ pub fn prepare_uinodes( } } ui_meta.vertices.write_buffer(&render_device, &render_queue); + ui_meta.indices.write_buffer(&render_device, &render_queue); *previous_len = batches.len(); commands.insert_or_spawn_batch(batches); } diff --git a/crates/bevy_ui/src/render/pipeline.rs b/crates/bevy_ui/src/render/pipeline.rs index 6dad2b104c3bb9..e31dc3bcfbba98 100644 --- a/crates/bevy_ui/src/render/pipeline.rs +++ b/crates/bevy_ui/src/render/pipeline.rs @@ -65,6 +65,12 @@ impl SpecializedRenderPipeline for UiPipeline { VertexFormat::Float32x4, // mode VertexFormat::Uint32, + // border radius + VertexFormat::Float32x4, + // border thickness + VertexFormat::Float32x4, + // border size + VertexFormat::Float32x2, ], ); let shader_defs = Vec::new(); diff --git a/crates/bevy_ui/src/render/render_pass.rs b/crates/bevy_ui/src/render/render_pass.rs index 1f3ffb0d20dea3..892186478e678e 100644 --- a/crates/bevy_ui/src/render/render_pass.rs +++ b/crates/bevy_ui/src/render/render_pass.rs @@ -215,8 +215,17 @@ impl RenderCommand

for DrawUiNode { return RenderCommandResult::Failure; }; - pass.set_vertex_buffer(0, ui_meta.into_inner().vertices.buffer().unwrap().slice(..)); - pass.draw(batch.range.clone(), 0..1); + let ui_meta = ui_meta.into_inner(); + // Store the vertices + pass.set_vertex_buffer(0, ui_meta.vertices.buffer().unwrap().slice(..)); + // Define how to "connect" the vertices + pass.set_index_buffer( + ui_meta.indices.buffer().unwrap().slice(..), + 0, + bevy_render::render_resource::IndexFormat::Uint32, + ); + // Draw the vertices + pass.draw_indexed(batch.range.clone(), 0, 0..1); RenderCommandResult::Success } } diff --git a/crates/bevy_ui/src/render/ui.wgsl b/crates/bevy_ui/src/render/ui.wgsl index aeb57aad81358e..7a73382a650ce3 100644 --- a/crates/bevy_ui/src/render/ui.wgsl +++ b/crates/bevy_ui/src/render/ui.wgsl @@ -1,13 +1,27 @@ #import bevy_render::view::View -const TEXTURED_QUAD: u32 = 0u; +const TEXTURED = 1u; +const RIGHT_VERTEX = 2u; +const BOTTOM_VERTEX = 4u; +const BORDER: u32 = 8u; + +fn enabled(flags: u32, mask: u32) -> bool { + return (flags & mask) != 0u; +} @group(0) @binding(0) var view: View; struct VertexOutput { @location(0) uv: vec2, @location(1) color: vec4, - @location(3) @interpolate(flat) mode: u32, + + @location(2) @interpolate(flat) size: vec2, + @location(3) @interpolate(flat) flags: u32, + @location(4) @interpolate(flat) radius: vec4, + @location(5) @interpolate(flat) border: vec4, + + // Position relative to the center of the rectangle. + @location(6) point: vec2, @builtin(position) position: vec4, }; @@ -16,27 +30,285 @@ fn vertex( @location(0) vertex_position: vec3, @location(1) vertex_uv: vec2, @location(2) vertex_color: vec4, - @location(3) mode: u32, + @location(3) flags: u32, + + // x: top left, y: top right, z: bottom right, w: bottom left. + @location(4) radius: vec4, + + // x: left, y: top, z: right, w: bottom. + @location(5) border: vec4, + @location(6) size: vec2, ) -> VertexOutput { var out: VertexOutput; out.uv = vertex_uv; - out.position = view.view_proj * vec4(vertex_position, 1.0); + out.position = view.view_proj * vec4(vertex_position, 1.0); out.color = vertex_color; - out.mode = mode; + out.flags = flags; + out.radius = radius; + out.size = size; + out.border = border; + var point = 0.49999 * size; + if (flags & RIGHT_VERTEX) == 0u { + point.x *= -1.; + } + if (flags & BOTTOM_VERTEX) == 0u { + point.y *= -1.; + } + out.point = point; + return out; } @group(1) @binding(0) var sprite_texture: texture_2d; @group(1) @binding(1) var sprite_sampler: sampler; -@fragment -fn fragment(in: VertexOutput) -> @location(0) vec4 { - // textureSample can only be called in unform control flow, not inside an if branch. - var color = textureSample(sprite_texture, sprite_sampler, in.uv); - if in.mode == TEXTURED_QUAD { - color = in.color * color; +fn sigmoid(t: f32) -> f32 { + return 1.0 / (1.0 + exp(-t)); +} + +// The returned value is the shortest distance from the given point to the boundary of the rounded +// box. +// +// Negative values indicate that the point is inside the rounded box, positive values that the point +// is outside, and zero is exactly on the boundary. +// +// Arguments: +// - `point` -> The function will return the distance from this point to the closest point on +// the boundary. +// - `size` -> The maximum width and height of the box. +// - `corner_radii` -> The radius of each rounded corner. Ordered counter clockwise starting +// top left: +// x: top left, y: top right, z: bottom right, w: bottom left. +fn sd_rounded_box(point: vec2, size: vec2, corner_radii: vec4) -> f32 { + // If 0.0 < y then select bottom left (w) and bottom right corner radius (z). + // Else select top left (x) and top right corner radius (y). + let rs = select(corner_radii.xy, corner_radii.wz, 0.0 < point.y); + // w and z are swapped so that both pairs are in left to right order, otherwise this second + // select statement would return the incorrect value for the bottom pair. + let radius = select(rs.x, rs.y, 0.0 < point.x); + // Vector from the corner closest to the point, to the point. + let corner_to_point = abs(point) - 0.5 * size; + // Vector from the center of the radius circle to the point. + let q = corner_to_point + radius; + // Length from center of the radius circle to the point, zeros a component if the point is not + // within the quadrant of the radius circle that is part of the curved corner. + let l = length(max(q, vec2(0.0))); + let m = min(max(q.x, q.y), 0.0); + return l + m - radius; +} + +fn sd_inset_rounded_box(point: vec2, size: vec2, radius: vec4, inset: vec4) -> f32 { + let inner_size = size - inset.xy - inset.zw; + let inner_center = inset.xy + 0.5 * inner_size - 0.5 * size; + let inner_point = point - inner_center; + + var r = radius; + + // Top left corner. + r.x = r.x - max(inset.x, inset.y); + + // Top right corner. + r.y = r.y - max(inset.z, inset.y); + + // Bottom right corner. + r.z = r.z - max(inset.z, inset.w); + + // Bottom left corner. + r.w = r.w - max(inset.x, inset.w); + + let half_size = inner_size * 0.5; + let min = min(half_size.x, half_size.y); + + r = min(max(r, vec4(0.0)), vec4(min)); + + return sd_rounded_box(inner_point, inner_size, r); +} + +#ifdef CLAMP_INNER_CURVES +fn sd_inset_rounded_box(point: vec2, size: vec2, radius: vec4, inset: vec4) -> f32 { + let inner_size = size - inset.xy - inset.zw; + let inner_center = inset.xy + 0.5 * inner_size - 0.5 * size; + let inner_point = point - inner_center; + + var r = radius; + + if 0. < min(inset.x, inset.y) || inset.x + inset.y <= 0. { + // Top left corner. + r.x = r.x - max(inset.x, inset.y); + } else { + r.x = 0.; + } + + if 0. < min(inset.z, inset.y) || inset.z + inset.y <= 0. { + // Top right corner. + r.y = r.y - max(inset.z, inset.y); + } else { + r.y = 0.; + } + + if 0. < min(inset.z, inset.w) || inset.z + inset.w <= 0. { + // Bottom right corner. + r.z = r.z - max(inset.z, inset.w); } else { - color = in.color; + r.z = 0.; } - return color; + + if 0. < min(inset.x, inset.w) || inset.x + inset.w <= 0. { + // Bottom left corner. + r.w = r.w - max(inset.x, inset.w); + } else { + r.w = 0.; + } + + let half_size = inner_size * 0.5; + let min = min(half_size.x, half_size.y); + + r = min(max(r, vec4(0.0)), vec4(min)); + + return sd_rounded_box(inner_point, inner_size, r); +} +#endif + +const RED: vec4 = vec4(1., 0., 0., 1.); +const GREEN: vec4 = vec4(0., 1., 0., 1.); +const BLUE: vec4 = vec4(0., 0., 1., 1.); +const WHITE = vec4(1., 1., 1., 1.); +const BLACK = vec4(0., 0., 0., 1.); + +// Draw the border in white, rest of the rect black. +fn draw_border(in: VertexOutput) -> vec4 { + // Distance from external border. Positive values outside. + let external_distance = sd_rounded_box(in.point, in.size, in.radius); + + // Distance from internal border. Positive values inside. + let internal_distance = sd_inset_rounded_box(in.point, in.size, in.radius, in.border); + + // Distance from border, positive values inside border. + let border = max(-internal_distance, external_distance); + + if border < 0.0 { + return WHITE; + } else { + return BLACK; + } +} + +// Draw just the interior in white, rest of the rect black. +fn draw_interior(in: VertexOutput) -> vec4 { + // Distance from external border. Positive values outside. + let external_distance = sd_rounded_box(in.point, in.size, in.radius); + + if external_distance < 0.0 { + return WHITE; + } else { + return BLACK; + } +} + +// Draw all the geometry. +fn draw_test(in: VertexOutput) -> vec4 { + // Distance from external border. Negative inside + let external_distance = sd_rounded_box(in.point, in.size, in.radius); + + // Distance from internal border. + let internal_distance = sd_inset_rounded_box(in.point, in.size, in.radius, in.border); + + // Distance from border. + let border = max(-internal_distance, external_distance); + + // Draw the area outside the border in green. + if 0.0 < external_distance { + return GREEN; + } + + // Draw the area inside the border in white. + if border < 0.0 { + return WHITE; + } + + // Draw the interior in blue. + if internal_distance < 0.0 { + return BLUE; + } + + // Fill anything else with red (the presence of any red is a bug). + return RED; +} + +fn draw_no_aa(in: VertexOutput) -> vec4 { + let texture_color = textureSample(sprite_texture, sprite_sampler, in.uv); + let color = select(in.color, in.color * texture_color, enabled(in.flags, TEXTURED)); + + // Negative value => point inside external border. + let external_distance = sd_rounded_box(in.point, in.size, in.radius); + // Negative value => point inside internal border. + let internal_distance = sd_inset_rounded_box(in.point, in.size, in.radius, in.border); + // Negative value => point inside border. + let border = max(external_distance, -internal_distance); + + if enabled(in.flags, BORDER) { + if border < 0.0 { + return color; + } else { + return vec4(0.0); + } + } + + if external_distance < 0.0 { + return color; + } + + return vec4(0.0); +} + +fn draw(in: VertexOutput) -> vec4 { + let texture_color = textureSample(sprite_texture, sprite_sampler, in.uv); + + // Only use the color sampled from the texture if the `TEXTURED` flag is enabled. + // This allows us to draw both textured and untextured shapes together in the same batch. + let color = select(in.color, in.color * texture_color, enabled(in.flags, TEXTURED)); + + // Signed distances. The magnitude is the distance of the point from the edge of the shape. + // * Negative values indicate that the point is inside the shape. + // * Zero values indicate the point is on on the edge of the shape. + // * Positive values indicate the point is outside the shape. + + // Signed distance from the exterior boundary. + let external_distance = sd_rounded_box(in.point, in.size, in.radius); + + // Signed distance from the border's internal edge (the signed distance is negative if the point + // is inside the rect but not on the border). + // If the border size is set to zero, this is the same as as the external distance. + let internal_distance = sd_inset_rounded_box(in.point, in.size, in.radius, in.border); + + // Signed distance from the border (the intersection of the rect with its border). + // Points inside the border have negative signed distance. Any point outside the border, whether + // outside the outside edge, or inside the inner edge have positive signed distance. + let border_distance = max(external_distance, -internal_distance); + + // The `fwidth` function returns an approximation of the rate of change of the signed distance + // value that is used to ensure that the smooth alpha transition created by smoothstep occurs + // over a range of distance values that is proportional to how quickly the distance is changing. + let fborder = fwidth(border_distance); + let fexternal = fwidth(external_distance); + + if enabled(in.flags, BORDER) { + // The item is a border + + // At external edges with no border, `border_distance` is equal to zero. + // This select statement ensures we only perform anti-aliasing where a non-zero width border + // is present, otherwise an outline about the external boundary would be drawn even without + // a border. + let t = 1. - select(step(0.0, border_distance), smoothstep(0.0, fborder, border_distance), external_distance < internal_distance); + return vec4(color.rgb * t * color.a, t * color.a); + } + + // The item is a rectangle, draw normally with anti-aliasing at the edges. + let t = 1. - smoothstep(0.0, fexternal, external_distance); + return vec4(color.rgb * t * color.a, t * color.a); +} + +@fragment +fn fragment(in: VertexOutput) -> @location(0) vec4 { + return draw(in); } diff --git a/crates/bevy_ui/src/texture_slice.rs b/crates/bevy_ui/src/texture_slice.rs index c0836a3b870bb6..16cfacff766701 100644 --- a/crates/bevy_ui/src/texture_slice.rs +++ b/crates/bevy_ui/src/texture_slice.rs @@ -10,7 +10,7 @@ use bevy_sprite::{ImageScaleMode, TextureAtlas, TextureAtlasLayout, TextureSlice use bevy_transform::prelude::*; use bevy_utils::HashSet; -use crate::{CalculatedClip, ExtractedUiNode, Node, UiImage}; +use crate::{CalculatedClip, ExtractedUiNode, Node, NodeType, UiImage}; /// Component storing texture slices for image nodes entities with a tiled or sliced [`ImageScaleMode`] /// @@ -68,6 +68,9 @@ impl ComputedTextureSlices { atlas_size, clip: clip.map(|clip| clip.clip), camera_entity, + border: [0.; 4], + border_radius: [0.; 4], + node_type: NodeType::Rect, } }) } diff --git a/crates/bevy_ui/src/ui_node.rs b/crates/bevy_ui/src/ui_node.rs index f7b77149e60a87..272220f3d52ae9 100644 --- a/crates/bevy_ui/src/ui_node.rs +++ b/crates/bevy_ui/src/ui_node.rs @@ -1812,6 +1812,268 @@ impl Default for ZIndex { } } +/// Used to add rounded corners to a UI node. You can set a UI node to have uniformly +/// rounded corners or specify different radii for each corner. If a given radius exceeds half +/// the length of the smallest dimension between the node's height or width, the radius will +/// calculated as half the smallest dimension. +/// +/// Elliptical nodes are not supported yet. Percentage values are based on the node's smallest +/// dimension, either width or height. +/// +/// # Example +/// ``` +/// # use bevy_ecs::prelude::*; +/// # use bevy_ui::prelude::*; +/// # use bevy_color::palettes::basic::{BLUE}; +/// fn setup_ui(mut commands: Commands) { +/// commands.spawn(( +/// NodeBundle { +/// style: Style { +/// width: Val::Px(100.), +/// height: Val::Px(100.), +/// border: UiRect::all(Val::Px(2.)), +/// ..Default::default() +/// }, +/// background_color: BLUE.into(), +/// border_radius: BorderRadius::new( +/// // top left +/// Val::Px(10.), +/// // top right +/// Val::Px(20.), +/// // bottom right +/// Val::Px(30.), +/// // bottom left +/// Val::Px(40.), +/// ), +/// ..Default::default() +/// }, +/// )); +/// } +/// ``` +/// +/// +#[derive(Component, Copy, Clone, Debug, PartialEq, Reflect)] +#[reflect(PartialEq, Default)] +#[cfg_attr( + feature = "serialize", + derive(serde::Serialize, serde::Deserialize), + reflect(Serialize, Deserialize) +)] +pub struct BorderRadius { + pub top_left: Val, + pub top_right: Val, + pub bottom_left: Val, + pub bottom_right: Val, +} + +impl Default for BorderRadius { + fn default() -> Self { + Self::DEFAULT + } +} + +impl BorderRadius { + pub const DEFAULT: Self = Self::ZERO; + + /// Zero curvature. All the corners will be right-angled. + pub const ZERO: Self = Self::all(Val::Px(0.)); + + /// Maximum curvature. The UI Node will take a capsule shape or circular if width and height are equal. + pub const MAX: Self = Self::all(Val::Px(f32::MAX)); + + #[inline] + /// Set all four corners to the same curvature. + pub const fn all(radius: Val) -> Self { + Self { + top_left: radius, + top_right: radius, + bottom_left: radius, + bottom_right: radius, + } + } + + #[inline] + pub const fn new(top_left: Val, top_right: Val, bottom_right: Val, bottom_left: Val) -> Self { + Self { + top_left, + top_right, + bottom_right, + bottom_left, + } + } + + #[inline] + /// Sets the radii to logical pixel values. + pub const fn px(top_left: f32, top_right: f32, bottom_right: f32, bottom_left: f32) -> Self { + Self { + top_left: Val::Px(top_left), + top_right: Val::Px(top_right), + bottom_right: Val::Px(bottom_right), + bottom_left: Val::Px(bottom_left), + } + } + + #[inline] + /// Sets the radii to percentage values. + pub const fn percent( + top_left: f32, + top_right: f32, + bottom_right: f32, + bottom_left: f32, + ) -> Self { + Self { + top_left: Val::Px(top_left), + top_right: Val::Px(top_right), + bottom_right: Val::Px(bottom_right), + bottom_left: Val::Px(bottom_left), + } + } + + #[inline] + /// Sets the radius for the top left corner. + /// Remaining corners will be right-angled. + pub const fn top_left(radius: Val) -> Self { + Self { + top_left: radius, + ..Self::DEFAULT + } + } + + #[inline] + /// Sets the radius for the top right corner. + /// Remaining corners will be right-angled. + pub const fn top_right(radius: Val) -> Self { + Self { + top_right: radius, + ..Self::DEFAULT + } + } + + #[inline] + /// Sets the radius for the bottom right corner. + /// Remaining corners will be right-angled. + pub const fn bottom_right(radius: Val) -> Self { + Self { + bottom_right: radius, + ..Self::DEFAULT + } + } + + #[inline] + /// Sets the radius for the bottom left corner. + /// Remaining corners will be right-angled. + pub const fn bottom_left(radius: Val) -> Self { + Self { + bottom_left: radius, + ..Self::DEFAULT + } + } + + #[inline] + /// Sets the radii for the top left and bottom left corners. + /// Remaining corners will be right-angled. + pub const fn left(radius: Val) -> Self { + Self { + top_left: radius, + bottom_left: radius, + ..Self::DEFAULT + } + } + + #[inline] + /// Sets the radii for the top right and bottom right corners. + /// Remaining corners will be right-angled. + pub const fn right(radius: Val) -> Self { + Self { + top_right: radius, + bottom_right: radius, + ..Self::DEFAULT + } + } + + #[inline] + /// Sets the radii for the top left and top right corners. + /// Remaining corners will be right-angled. + pub const fn top(radius: Val) -> Self { + Self { + top_left: radius, + top_right: radius, + ..Self::DEFAULT + } + } + + #[inline] + /// Sets the radii for the bottom left and bottom right corners. + /// Remaining corners will be right-angled. + pub const fn bottom(radius: Val) -> Self { + Self { + bottom_left: radius, + bottom_right: radius, + ..Self::DEFAULT + } + } + + /// Returns the [`BorderRadius`] with its `top_left` field set to the given value. + #[inline] + pub const fn with_top_left(mut self, radius: Val) -> Self { + self.top_left = radius; + self + } + + /// Returns the [`BorderRadius`] with its `top_right` field set to the given value. + #[inline] + pub const fn with_top_right(mut self, radius: Val) -> Self { + self.top_right = radius; + self + } + + /// Returns the [`BorderRadius`] with its `bottom_right` field set to the given value. + #[inline] + pub const fn with_bottom_right(mut self, radius: Val) -> Self { + self.bottom_right = radius; + self + } + + /// Returns the [`BorderRadius`] with its `bottom_left` field set to the given value. + #[inline] + pub const fn with_bottom_left(mut self, radius: Val) -> Self { + self.bottom_left = radius; + self + } + + /// Returns the [`BorderRadius`] with its `top_left` and `bottom_left` fields set to the given value. + #[inline] + pub const fn with_left(mut self, radius: Val) -> Self { + self.top_left = radius; + self.bottom_left = radius; + self + } + + /// Returns the [`BorderRadius`] with its `top_right` and `bottom_right` fields set to the given value. + #[inline] + pub const fn with_right(mut self, radius: Val) -> Self { + self.top_right = radius; + self.bottom_right = radius; + self + } + + /// Returns the [`BorderRadius`] with its `top_left` and `top_right` fields set to the given value. + #[inline] + pub const fn with_top(mut self, radius: Val) -> Self { + self.top_left = radius; + self.top_right = radius; + self + } + + /// Returns the [`BorderRadius`] with its `bottom_left` and `bottom_right` fields set to the given value. + #[inline] + pub const fn with_bottom(mut self, radius: Val) -> Self { + self.bottom_left = radius; + self.bottom_right = radius; + self + } +} + #[cfg(test)] mod tests { use crate::GridPlacement; diff --git a/crates/bevy_utils/src/lib.rs b/crates/bevy_utils/src/lib.rs index 33f53121710412..d142f43fbc9ef9 100644 --- a/crates/bevy_utils/src/lib.rs +++ b/crates/bevy_utils/src/lib.rs @@ -36,21 +36,36 @@ use hashbrown::hash_map::RawEntryMut; use std::{ any::TypeId, fmt::Debug, - future::Future, hash::{BuildHasher, BuildHasherDefault, Hash, Hasher}, marker::PhantomData, mem::ManuallyDrop, ops::Deref, - pin::Pin, }; -/// An owned and dynamically typed Future used when you can't statically type your result or need to add some indirection. #[cfg(not(target_arch = "wasm32"))] -pub type BoxedFuture<'a, T> = Pin + Send + 'a>>; +mod conditional_send { + /// Use [`ConditionalSend`] to mark an optional Send trait bound. Useful as on certain platforms (eg. WASM), + /// futures aren't Send. + pub trait ConditionalSend: Send {} + impl ConditionalSend for T {} +} -#[allow(missing_docs)] #[cfg(target_arch = "wasm32")] -pub type BoxedFuture<'a, T> = Pin + 'a>>; +#[allow(missing_docs)] +mod conditional_send { + pub trait ConditionalSend {} + impl ConditionalSend for T {} +} + +pub use conditional_send::*; + +/// Use [`ConditionalSendFuture`] for a future with an optional Send trait bound, as on certain platforms (eg. WASM), +/// futures aren't Send. +pub trait ConditionalSendFuture: std::future::Future + ConditionalSend {} +impl ConditionalSendFuture for T {} + +/// An owned and dynamically typed Future used when you can't statically type your result or need to add some indirection. +pub type BoxedFuture<'a, T> = std::pin::Pin + 'a>>; /// A shortcut alias for [`hashbrown::hash_map::Entry`]. pub type Entry<'a, K, V, S = BuildHasherDefault> = hashbrown::hash_map::Entry<'a, K, V, S>; diff --git a/crates/bevy_window/src/window.rs b/crates/bevy_window/src/window.rs index c241c9b71330ae..ccc861a78d23b0 100644 --- a/crates/bevy_window/src/window.rs +++ b/crates/bevy_window/src/window.rs @@ -259,6 +259,17 @@ pub struct Window { /// /// - **Android / Wayland / Web:** Unsupported. pub visible: bool, + /// Sets whether the window should be shown in the taskbar. + /// + /// If `true`, the window will not appear in the taskbar. + /// If `false`, the window will appear in the taskbar. + /// + /// Note that this will only take effect on window creation. + /// + /// ## Platform-specific + /// + /// - Only supported on Windows. + pub skip_taskbar: bool, } impl Default for Window { @@ -287,6 +298,7 @@ impl Default for Window { canvas: None, window_theme: None, visible: true, + skip_taskbar: false, } } } diff --git a/crates/bevy_winit/src/winit_windows.rs b/crates/bevy_winit/src/winit_windows.rs index 4375e7c2777448..ff7d798d4bf337 100644 --- a/crates/bevy_winit/src/winit_windows.rs +++ b/crates/bevy_winit/src/winit_windows.rs @@ -104,6 +104,12 @@ impl WinitWindows { .with_transparent(window.transparent) .with_visible(window.visible); + #[cfg(target_os = "windows")] + { + use winit::platform::windows::WindowBuilderExtWindows; + winit_window_builder = winit_window_builder.with_skip_taskbar(window.skip_taskbar) + } + #[cfg(any( target_os = "linux", target_os = "dragonfly", diff --git a/examples/3d/lighting.rs b/examples/3d/lighting.rs index 4f54eb9b1416df..001b66bc8d415c 100644 --- a/examples/3d/lighting.rs +++ b/examples/3d/lighting.rs @@ -99,7 +99,7 @@ fn setup( PbrBundle { mesh: meshes.add(Cuboid::default()), material: materials.add(StandardMaterial { - base_color: PINK.into(), + base_color: DEEP_PINK.into(), ..default() }), transform: Transform::from_xyz(0.0, 0.5, 0.0), diff --git a/examples/3d/wireframe.rs b/examples/3d/wireframe.rs index aaeb42393b3188..f78a454a544adf 100644 --- a/examples/3d/wireframe.rs +++ b/examples/3d/wireframe.rs @@ -146,7 +146,7 @@ Color: {:?} // Toggle the global wireframe color if keyboard_input.just_pressed(KeyCode::KeyX) { config.default_color = if config.default_color == WHITE.into() { - PINK.into() + DEEP_PINK.into() } else { WHITE.into() }; diff --git a/examples/README.md b/examples/README.md index 74795cbaeffe0e..0eca6e27e9355f 100644 --- a/examples/README.md +++ b/examples/README.md @@ -407,6 +407,7 @@ Example | Description [Overflow and Clipping Debug](../examples/ui/overflow_debug.rs) | An example to debug overflow and clipping behavior [Relative Cursor Position](../examples/ui/relative_cursor_position.rs) | Showcases the RelativeCursorPosition component [Render UI to Texture](../examples/ui/render_ui_to_texture.rs) | An example of rendering UI as a part of a 3D world +[Rounded Borders](../examples/ui/rounded_borders.rs) | Demonstrates how to create a node with a rounded border [Size Constraints](../examples/ui/size_constraints.rs) | Demonstrates how the to use the size constraints to control the size of a UI node. [Text](../examples/ui/text.rs) | Illustrates creating and updating text [Text Debug](../examples/ui/text_debug.rs) | An example for debugging text layout diff --git a/examples/asset/asset_decompression.rs b/examples/asset/asset_decompression.rs index cd17a38a89a9eb..f003471a72bb77 100644 --- a/examples/asset/asset_decompression.rs +++ b/examples/asset/asset_decompression.rs @@ -7,7 +7,6 @@ use bevy::{ }, prelude::*, reflect::TypePath, - utils::BoxedFuture, }; use flate2::read::GzDecoder; use std::io::prelude::*; @@ -41,44 +40,42 @@ impl AssetLoader for GzAssetLoader { type Asset = GzAsset; type Settings = (); type Error = GzAssetLoaderError; - fn load<'a>( + async fn load<'a>( &'a self, - reader: &'a mut Reader, + reader: &'a mut Reader<'_>, _settings: &'a (), - load_context: &'a mut LoadContext, - ) -> BoxedFuture<'a, Result> { - Box::pin(async move { - let compressed_path = load_context.path(); - let file_name = compressed_path - .file_name() - .ok_or(GzAssetLoaderError::IndeterminateFilePath)? - .to_string_lossy(); - let uncompressed_file_name = file_name - .strip_suffix(".gz") - .ok_or(GzAssetLoaderError::IndeterminateFilePath)?; - let contained_path = compressed_path.join(uncompressed_file_name); + load_context: &'a mut LoadContext<'_>, + ) -> Result { + let compressed_path = load_context.path(); + let file_name = compressed_path + .file_name() + .ok_or(GzAssetLoaderError::IndeterminateFilePath)? + .to_string_lossy(); + let uncompressed_file_name = file_name + .strip_suffix(".gz") + .ok_or(GzAssetLoaderError::IndeterminateFilePath)?; + let contained_path = compressed_path.join(uncompressed_file_name); - let mut bytes_compressed = Vec::new(); + let mut bytes_compressed = Vec::new(); - reader.read_to_end(&mut bytes_compressed).await?; + reader.read_to_end(&mut bytes_compressed).await?; - let mut decoder = GzDecoder::new(bytes_compressed.as_slice()); + let mut decoder = GzDecoder::new(bytes_compressed.as_slice()); - let mut bytes_uncompressed = Vec::new(); + let mut bytes_uncompressed = Vec::new(); - decoder.read_to_end(&mut bytes_uncompressed)?; + decoder.read_to_end(&mut bytes_uncompressed)?; - // Now that we have decompressed the asset, let's pass it back to the - // context to continue loading + // Now that we have decompressed the asset, let's pass it back to the + // context to continue loading - let mut reader = VecReader::new(bytes_uncompressed); + let mut reader = VecReader::new(bytes_uncompressed); - let uncompressed = load_context - .load_direct_with_reader(&mut reader, contained_path) - .await?; + let uncompressed = load_context + .load_direct_with_reader(&mut reader, contained_path) + .await?; - Ok(GzAsset { uncompressed }) - }) + Ok(GzAsset { uncompressed }) } fn extensions(&self) -> &[&str] { diff --git a/examples/asset/custom_asset.rs b/examples/asset/custom_asset.rs index c10e297c380bd9..b3167bbd1d8bb4 100644 --- a/examples/asset/custom_asset.rs +++ b/examples/asset/custom_asset.rs @@ -4,7 +4,6 @@ use bevy::{ asset::{io::Reader, ron, AssetLoader, AsyncReadExt, LoadContext}, prelude::*, reflect::TypePath, - utils::BoxedFuture, }; use serde::Deserialize; use thiserror::Error; @@ -34,18 +33,16 @@ impl AssetLoader for CustomAssetLoader { type Asset = CustomAsset; type Settings = (); type Error = CustomAssetLoaderError; - fn load<'a>( + async fn load<'a>( &'a self, - reader: &'a mut Reader, + reader: &'a mut Reader<'_>, _settings: &'a (), - _load_context: &'a mut LoadContext, - ) -> BoxedFuture<'a, Result> { - Box::pin(async move { - let mut bytes = Vec::new(); - reader.read_to_end(&mut bytes).await?; - let custom_asset = ron::de::from_bytes::(&bytes)?; - Ok(custom_asset) - }) + _load_context: &'a mut LoadContext<'_>, + ) -> Result { + let mut bytes = Vec::new(); + reader.read_to_end(&mut bytes).await?; + let custom_asset = ron::de::from_bytes::(&bytes)?; + Ok(custom_asset) } fn extensions(&self) -> &[&str] { @@ -75,19 +72,17 @@ impl AssetLoader for BlobAssetLoader { type Settings = (); type Error = BlobAssetLoaderError; - fn load<'a>( + async fn load<'a>( &'a self, - reader: &'a mut Reader, + reader: &'a mut Reader<'_>, _settings: &'a (), - _load_context: &'a mut LoadContext, - ) -> BoxedFuture<'a, Result> { - Box::pin(async move { - info!("Loading Blob..."); - let mut bytes = Vec::new(); - reader.read_to_end(&mut bytes).await?; - - Ok(Blob { bytes }) - }) + _load_context: &'a mut LoadContext<'_>, + ) -> Result { + info!("Loading Blob..."); + let mut bytes = Vec::new(); + reader.read_to_end(&mut bytes).await?; + + Ok(Blob { bytes }) } } diff --git a/examples/asset/custom_asset_reader.rs b/examples/asset/custom_asset_reader.rs index 4e4b0eede68db4..d302ebbf7f9fcb 100644 --- a/examples/asset/custom_asset_reader.rs +++ b/examples/asset/custom_asset_reader.rs @@ -3,42 +3,35 @@ //! It does not know anything about the asset formats, only how to talk to the underlying storage. use bevy::{ - asset::io::{AssetReader, AssetReaderError, AssetSource, AssetSourceId, PathStream, Reader}, + asset::io::{ + AssetReader, AssetReaderError, AssetSource, AssetSourceId, ErasedAssetReader, PathStream, + Reader, + }, prelude::*, - utils::BoxedFuture, }; use std::path::Path; /// A custom asset reader implementation that wraps a given asset reader implementation -struct CustomAssetReader(Box); +struct CustomAssetReader(Box); impl AssetReader for CustomAssetReader { - fn read<'a>( - &'a self, - path: &'a Path, - ) -> BoxedFuture<'a, Result>, AssetReaderError>> { + async fn read<'a>(&'a self, path: &'a Path) -> Result>, AssetReaderError> { info!("Reading {:?}", path); - self.0.read(path) + self.0.read(path).await } - fn read_meta<'a>( - &'a self, - path: &'a Path, - ) -> BoxedFuture<'a, Result>, AssetReaderError>> { - self.0.read_meta(path) + async fn read_meta<'a>(&'a self, path: &'a Path) -> Result>, AssetReaderError> { + self.0.read_meta(path).await } - fn read_directory<'a>( + async fn read_directory<'a>( &'a self, path: &'a Path, - ) -> BoxedFuture<'a, Result, AssetReaderError>> { - self.0.read_directory(path) + ) -> Result, AssetReaderError> { + self.0.read_directory(path).await } - fn is_directory<'a>( - &'a self, - path: &'a Path, - ) -> BoxedFuture<'a, Result> { - self.0.is_directory(path) + async fn is_directory<'a>(&'a self, path: &'a Path) -> Result { + self.0.is_directory(path).await } } diff --git a/examples/asset/processing/asset_processing.rs b/examples/asset/processing/asset_processing.rs index 3f660ef85f5003..f99fc4769a4ca9 100644 --- a/examples/asset/processing/asset_processing.rs +++ b/examples/asset/processing/asset_processing.rs @@ -12,7 +12,6 @@ use bevy::{ }, prelude::*, reflect::TypePath, - utils::BoxedFuture, }; use serde::{Deserialize, Serialize}; use std::convert::Infallible; @@ -83,22 +82,20 @@ impl AssetLoader for TextLoader { type Asset = Text; type Settings = TextSettings; type Error = std::io::Error; - fn load<'a>( + async fn load<'a>( &'a self, - reader: &'a mut Reader, + reader: &'a mut Reader<'_>, settings: &'a TextSettings, - _load_context: &'a mut LoadContext, - ) -> BoxedFuture<'a, Result> { - Box::pin(async move { - let mut bytes = Vec::new(); - reader.read_to_end(&mut bytes).await?; - let value = if let Some(ref text) = settings.text_override { - text.clone() - } else { - String::from_utf8(bytes).unwrap() - }; - Ok(Text(value)) - }) + _load_context: &'a mut LoadContext<'_>, + ) -> Result { + let mut bytes = Vec::new(); + reader.read_to_end(&mut bytes).await?; + let value = if let Some(ref text) = settings.text_override { + text.clone() + } else { + String::from_utf8(bytes).unwrap() + }; + Ok(Text(value)) } fn extensions(&self) -> &[&str] { @@ -138,30 +135,28 @@ impl AssetLoader for CoolTextLoader { type Settings = (); type Error = CoolTextLoaderError; - fn load<'a>( + async fn load<'a>( &'a self, - reader: &'a mut Reader, + reader: &'a mut Reader<'_>, _settings: &'a Self::Settings, - load_context: &'a mut LoadContext, - ) -> BoxedFuture<'a, Result> { - Box::pin(async move { - let mut bytes = Vec::new(); - reader.read_to_end(&mut bytes).await?; - let ron: CoolTextRon = ron::de::from_bytes(&bytes)?; - let mut base_text = ron.text; - for embedded in ron.embedded_dependencies { - let loaded = load_context.load_direct(&embedded).await?; - let text = loaded.get::().unwrap(); - base_text.push_str(&text.0); - } - Ok(CoolText { - text: base_text, - dependencies: ron - .dependencies - .iter() - .map(|p| load_context.load(p)) - .collect(), - }) + load_context: &'a mut LoadContext<'_>, + ) -> Result { + let mut bytes = Vec::new(); + reader.read_to_end(&mut bytes).await?; + let ron: CoolTextRon = ron::de::from_bytes(&bytes)?; + let mut base_text = ron.text; + for embedded in ron.embedded_dependencies { + let loaded = load_context.load_direct(&embedded).await?; + let text = loaded.get::().unwrap(); + base_text.push_str(&text.0); + } + Ok(CoolText { + text: base_text, + dependencies: ron + .dependencies + .iter() + .map(|p| load_context.load(p)) + .collect(), }) } @@ -184,15 +179,13 @@ impl AssetTransformer for CoolTextTransformer { type Settings = CoolTextTransformerSettings; type Error = Infallible; - fn transform<'a>( + async fn transform<'a>( &'a self, mut asset: TransformedAsset, settings: &'a Self::Settings, - ) -> BoxedFuture<'a, Result, Self::Error>> { - Box::pin(async move { - asset.text = format!("{}{}", asset.text, settings.appended); - Ok(asset) - }) + ) -> Result, Self::Error> { + asset.text = format!("{}{}", asset.text, settings.appended); + Ok(asset) } } @@ -204,16 +197,14 @@ impl AssetSaver for CoolTextSaver { type OutputLoader = TextLoader; type Error = std::io::Error; - fn save<'a>( + async fn save<'a>( &'a self, writer: &'a mut Writer, asset: SavedAsset<'a, Self::Asset>, _settings: &'a Self::Settings, - ) -> BoxedFuture<'a, Result> { - Box::pin(async move { - writer.write_all(asset.text.as_bytes()).await?; - Ok(TextSettings::default()) - }) + ) -> Result { + writer.write_all(asset.text.as_bytes()).await?; + Ok(TextSettings::default()) } } diff --git a/examples/dev_tools/fps_overlay.rs b/examples/dev_tools/fps_overlay.rs index e7f4cada954628..a2718ecc3c5c0d 100644 --- a/examples/dev_tools/fps_overlay.rs +++ b/examples/dev_tools/fps_overlay.rs @@ -28,30 +28,33 @@ fn main() { } fn setup(mut commands: Commands) { - // We need to spawn camera to see overlay + // We need to spawn a camera (2d or 3d) to see the overlay commands.spawn(Camera2dBundle::default()); - commands.spawn( - TextBundle::from_sections([ - TextSection::new( - "Press 1 to change color of the overlay.", - TextStyle { - font_size: 25.0, - ..default() - }, - ), - TextSection::new( - "\nPress 2 to change size of the overlay", + + // Instruction text + commands + .spawn(NodeBundle { + style: Style { + width: Val::Percent(100.0), + height: Val::Percent(100.0), + align_items: AlignItems::Center, + justify_content: JustifyContent::Center, + ..default() + }, + ..default() + }) + .with_children(|c| { + c.spawn(TextBundle::from_section( + concat!( + "Press 1 to change color of the overlay.\n", + "Press 2 to change size of the overlay." + ), TextStyle { font_size: 25.0, ..default() }, - ), - ]) - .with_style(Style { - justify_self: JustifySelf::Center, - ..default() - }), - ); + )); + }); } fn customize_config(input: Res>, mut overlay: ResMut) { diff --git a/examples/stress_tests/many_lights.rs b/examples/stress_tests/many_lights.rs index b914aa657c6df7..4157869197d67d 100644 --- a/examples/stress_tests/many_lights.rs +++ b/examples/stress_tests/many_lights.rs @@ -4,7 +4,7 @@ use std::f64::consts::PI; use bevy::{ - color::palettes::css::PINK, + color::palettes::css::DEEP_PINK, diagnostic::{FrameTimeDiagnosticsPlugin, LogDiagnosticsPlugin}, math::{DVec2, DVec3}, pbr::{ExtractedPointLight, GlobalLightMeta}, @@ -62,7 +62,7 @@ fn setup( let mesh = meshes.add(Cuboid::default()); let material = materials.add(StandardMaterial { - base_color: PINK.into(), + base_color: DEEP_PINK.into(), ..default() }); diff --git a/examples/ui/button.rs b/examples/ui/button.rs index c64a00ccbead9b..c00242fc88a2ae 100644 --- a/examples/ui/button.rs +++ b/examples/ui/button.rs @@ -74,6 +74,7 @@ fn setup(mut commands: Commands, asset_server: Res) { ..default() }, border_color: BorderColor(Color::BLACK), + border_radius: BorderRadius::MAX, image: UiImage::default().with_color(NORMAL_BUTTON), ..default() }) diff --git a/examples/ui/grid.rs b/examples/ui/grid.rs index 201912647bdf93..20c5753968c553 100644 --- a/examples/ui/grid.rs +++ b/examples/ui/grid.rs @@ -109,7 +109,7 @@ fn spawn_layout(mut commands: Commands, asset_server: Res) { item_rect(builder, CRIMSON); item_rect(builder, ANTIQUE_WHITE); item_rect(builder, YELLOW); - item_rect(builder, PINK); + item_rect(builder, DEEP_PINK); item_rect(builder, YELLOW_GREEN); item_rect(builder, SALMON); }); diff --git a/examples/ui/rounded_borders.rs b/examples/ui/rounded_borders.rs new file mode 100644 index 00000000000000..427f73455a751e --- /dev/null +++ b/examples/ui/rounded_borders.rs @@ -0,0 +1,176 @@ +//! Example demonstrating rounded bordered UI nodes + +use bevy::{color::palettes::css::*, prelude::*}; + +fn main() { + App::new() + .add_plugins(DefaultPlugins) + .add_systems(Startup, setup) + .run(); +} + +fn setup(mut commands: Commands) { + commands.spawn(Camera2dBundle::default()); + let root = commands + .spawn(NodeBundle { + style: Style { + margin: UiRect::all(Val::Px(25.0)), + align_self: AlignSelf::Stretch, + justify_self: JustifySelf::Stretch, + flex_wrap: FlexWrap::Wrap, + justify_content: JustifyContent::FlexStart, + align_items: AlignItems::FlexStart, + align_content: AlignContent::FlexStart, + ..Default::default() + }, + background_color: Color::srgb(0.25, 0.25, 0.25).into(), + ..Default::default() + }) + .id(); + + // labels for the different border edges + let border_labels = [ + "None", + "All", + "Left", + "Right", + "Top", + "Bottom", + "Left Right", + "Top Bottom", + "Top Left", + "Bottom Left", + "Top Right", + "Bottom Right", + "Top Bottom Right", + "Top Bottom Left", + "Top Left Right", + "Bottom Left Right", + ]; + + // all the different combinations of border edges + // these correspond to the labels above + let borders = [ + UiRect::default(), + UiRect::all(Val::Px(10.)), + UiRect::left(Val::Px(10.)), + UiRect::right(Val::Px(10.)), + UiRect::top(Val::Px(10.)), + UiRect::bottom(Val::Px(10.)), + UiRect::horizontal(Val::Px(10.)), + UiRect::vertical(Val::Px(10.)), + UiRect { + left: Val::Px(10.), + top: Val::Px(10.), + ..Default::default() + }, + UiRect { + left: Val::Px(10.), + bottom: Val::Px(10.), + ..Default::default() + }, + UiRect { + right: Val::Px(10.), + top: Val::Px(10.), + ..Default::default() + }, + UiRect { + right: Val::Px(10.), + bottom: Val::Px(10.), + ..Default::default() + }, + UiRect { + right: Val::Px(10.), + top: Val::Px(10.), + bottom: Val::Px(10.), + ..Default::default() + }, + UiRect { + left: Val::Px(10.), + top: Val::Px(10.), + bottom: Val::Px(10.), + ..Default::default() + }, + UiRect { + left: Val::Px(10.), + right: Val::Px(10.), + top: Val::Px(10.), + ..Default::default() + }, + UiRect { + left: Val::Px(10.), + right: Val::Px(10.), + bottom: Val::Px(10.), + ..Default::default() + }, + ]; + + for (label, border) in border_labels.into_iter().zip(borders) { + let inner_spot = commands + .spawn(NodeBundle { + style: Style { + width: Val::Px(10.), + height: Val::Px(10.), + ..Default::default() + }, + border_radius: BorderRadius::MAX, + background_color: YELLOW.into(), + ..Default::default() + }) + .id(); + let non_zero = |x, y| x != Val::Px(0.) && y != Val::Px(0.); + let border_size = |x, y| if non_zero(x, y) { f32::MAX } else { 0. }; + let border_radius = BorderRadius::px( + border_size(border.left, border.top), + border_size(border.right, border.top), + border_size(border.right, border.bottom), + border_size(border.left, border.bottom), + ); + let border_node = commands + .spawn(( + NodeBundle { + style: Style { + width: Val::Px(50.), + height: Val::Px(50.), + border, + margin: UiRect::all(Val::Px(20.)), + align_items: AlignItems::Center, + justify_content: JustifyContent::Center, + ..Default::default() + }, + background_color: MAROON.into(), + border_color: RED.into(), + border_radius, + ..Default::default() + }, + Outline { + width: Val::Px(6.), + offset: Val::Px(6.), + color: Color::WHITE, + }, + )) + .add_child(inner_spot) + .id(); + let label_node = commands + .spawn(TextBundle::from_section( + label, + TextStyle { + font_size: 9.0, + ..Default::default() + }, + )) + .id(); + let container = commands + .spawn(NodeBundle { + style: Style { + flex_direction: FlexDirection::Column, + align_items: AlignItems::Center, + ..Default::default() + }, + ..Default::default() + }) + .push_children(&[border_node, label_node]) + .id(); + commands.entity(root).add_child(container); + } +} diff --git a/examples/ui/ui.rs b/examples/ui/ui.rs index 3a314237af2a7c..585d69ee389d0e 100644 --- a/examples/ui/ui.rs +++ b/examples/ui/ui.rs @@ -12,18 +12,25 @@ use bevy::{ }; fn main() { - App::new() - .add_plugins(DefaultPlugins) + let mut app = App::new(); + app.add_plugins(DefaultPlugins) // Only run the app when there is user input. This will significantly reduce CPU/GPU use. .insert_resource(WinitSettings::desktop_app()) .add_systems(Startup, setup) - .add_systems(Update, mouse_scroll) - .run(); + .add_systems(Update, mouse_scroll); + + #[cfg(feature = "bevy_dev_tools")] + { + app.add_plugins(bevy::dev_tools::debug_overlay::DebugUiPlugin) + .add_systems(Update, toggle_overlay); + } + + app.run(); } fn setup(mut commands: Commands, asset_server: Res) { // Camera - commands.spawn(Camera2dBundle::default()); + commands.spawn((Camera2dBundle::default(), IsDefaultUiCamera)); // root node commands @@ -54,6 +61,7 @@ fn setup(mut commands: Commands, asset_server: Res) { .spawn(NodeBundle { style: Style { width: Val::Percent(100.), + flex_direction: FlexDirection::Column, ..default() }, background_color: Color::srgb(0.15, 0.15, 0.15).into(), @@ -79,6 +87,33 @@ fn setup(mut commands: Commands, asset_server: Res) { // for accessibility to treat the text accordingly. Label, )); + + #[cfg(feature = "bevy_dev_tools")] + // Debug overlay text + parent.spawn(( + TextBundle::from_section( + "Press Space to enable debug outlines.", + TextStyle { + font: asset_server.load("fonts/FiraSans-Bold.ttf"), + font_size: 20., + ..Default::default() + }, + ), + Label, + )); + + #[cfg(not(feature = "bevy_dev_tools"))] + parent.spawn(( + TextBundle::from_section( + "Try enabling feature \"bevy_dev_tools\".", + TextStyle { + font: asset_server.load("fonts/FiraSans-Bold.ttf"), + font_size: 20., + ..Default::default() + }, + ), + Label, + )); }); }); // right vertical fill @@ -334,3 +369,16 @@ fn mouse_scroll( } } } + +#[cfg(feature = "bevy_dev_tools")] +// The system that will enable/disable the debug outlines around the nodes +fn toggle_overlay( + input: Res>, + mut options: ResMut, +) { + info_once!("The debug outlines are enabled, press Space to turn them on/off"); + if input.just_pressed(KeyCode::Space) { + // The toggle method will enable the debug_overlay if disabled and disable if enabled + options.toggle(); + } +}