From 2c953914bcd13681447dbaec130dcce140a4aeb1 Mon Sep 17 00:00:00 2001 From: Stepan Koltsov Date: Mon, 18 Mar 2024 17:35:33 +0000 Subject: [PATCH 01/17] Explain Camera2dBundle.projection needs to be set carefully (#11115) Encountered it while implementing https://github.com/bevyengine/bevy/pull/11109. Co-authored-by: Alice Cecile --- crates/bevy_core_pipeline/src/core_2d/camera_2d.rs | 4 ++++ 1 file changed, 4 insertions(+) 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, From adb866947bb8574c2513963d078be20c4aa68beb Mon Sep 17 00:00:00 2001 From: Antony Date: Mon, 18 Mar 2024 13:41:42 -0400 Subject: [PATCH 02/17] Expose Winit's `with_skip_taskbar` on window creation (#12450) # Objective Resolves #12431. ## Solution Added a `skip_taskbar` field to the `Window` struct (defaults to `false`). Used in `create_windows` if the target OS is Windows. --- crates/bevy_window/src/window.rs | 12 ++++++++++++ crates/bevy_winit/src/winit_windows.rs | 6 ++++++ 2 files changed, 18 insertions(+) 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", From 3549ae9e37da66e92ce6704c247b5ee20eb23a5a Mon Sep 17 00:00:00 2001 From: Rob Parrett Date: Mon, 18 Mar 2024 10:44:46 -0700 Subject: [PATCH 03/17] Fix pink colors in examples (#12451) # Objective I was wondering why the `lighting` example was still looking quite different lately (specifically, the intensity of the green light on the cube) and noticed that we had one more color change I didn't catch before. Prior to the `bevy_color` port, `PINK` was actually "deep pink" from the css4 spec. `palettes::css::PINK` is now correctly a lighter pink color defined by the same spec. ```rust // Bevy 0.13 pub const PINK: Color = Color::rgb(1.0, 0.08, 0.58); // Bevy 0.14-dev pub const PINK: Srgba = Srgba::new(1.0, 0.753, 0.796, 1.0); pub const DEEP_PINK: Srgba = Srgba::new(1.0, 0.078, 0.576, 1.0); ``` ## Solution Change usages of `css::PINK` to `DEEP_PINK` to restore the examples to their former colors. --- examples/3d/lighting.rs | 2 +- examples/3d/wireframe.rs | 2 +- examples/stress_tests/many_lights.rs | 4 ++-- examples/ui/grid.rs | 2 +- 4 files changed, 5 insertions(+), 5 deletions(-) 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/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/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); }); From 26f2d3fb2fdd49d89cd8a3267998f46df2798736 Mon Sep 17 00:00:00 2001 From: robtfm <50659922+robtfm@users.noreply.github.com> Date: Mon, 18 Mar 2024 17:47:31 +0000 Subject: [PATCH 04/17] fast-fail in as_bind_group (#12513) # Objective prevent gpu buffer allocations when running `as_bind_group` for assets with texture dependencies that are not yet available. ## Solution reorder the binding creation so that fallible items are created first. --- crates/bevy_render/macros/src/as_bind_group.rs | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) 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({ From 70da903cec214c7615fe454731aea9e2fedf8a8a Mon Sep 17 00:00:00 2001 From: Multirious <77918086+Multirious@users.noreply.github.com> Date: Tue, 19 Mar 2024 00:49:58 +0700 Subject: [PATCH 05/17] Add methods to return the inner value for direction types (#12516) # Objective Currently in order to retrieve the inner values from direction types is that you need to use the `Deref` trait or `From`/`Into`. `Deref` that is currently implemented is an anti-pattern that I believe should be less relied upon. This pull-request add getters for retrieving the inner values for direction types. Advantages of getters: - Let rust-analyzer to list out available methods for users to understand better to on how to get the inner value. (This happens to me. I really don't know how to get the value until I look through the source code.) - They are simple. - Generally won't be ambiguous in most context. Traits such as `From`/`Into` will require fully qualified syntax from time to time. - Unsurprising result. Disadvantages of getters: - More verbose Advantages of deref polymorphism: - You reduce boilerplate for getting the value and call inner methods by: ```rust let dir = Dir3::new(Vec3::UP).unwrap(); // getting value let value = *dir; // instead of using getters let value = dir.vec3(); // calling methods for the inner vector dir.xy(); // instead of using getters dir.vec3().xy(); ``` Disadvantages of deref polymorphism: - When under more level of indirection, it will requires more dereferencing which will get ugly in some part: ```rust // getting value let value = **dir; // instead of using getters let value = dir.vec3(); // calling methods for the inner vector dir.xy(); // instead of using getters dir.vec3().xy(); ``` [More detail here](https://rust-unofficial.github.io/patterns/anti_patterns/deref.html). Edit: Update information for From/Into trait. Edit: Advantages and disadvantages. ## Solution Add `vec2` method for Dir2. Add `vec3` method for Dir3. Add `vec3a` method for Dir3A. --- crates/bevy_math/src/direction.rs | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/crates/bevy_math/src/direction.rs b/crates/bevy_math/src/direction.rs index c98736960c8401..a88d44b6024a4b 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 { @@ -287,6 +292,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 +457,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 { From ce75dec3b8972bf9feb922d3bf5594d70c5acb16 Mon Sep 17 00:00:00 2001 From: NiseVoid Date: Mon, 18 Mar 2024 18:54:41 +0100 Subject: [PATCH 06/17] Add setting to enable/disable shadows to MaterialPlugin (#12538) # Objective - Not all materials need shadow, but a queue_shadows system is always added to the `Render` schedule and executed ## Solution - Make a setting for shadows, it defaults to true ## Changelog - Added `shadows_enabled` setting to `MaterialPlugin` ## Migration Guide - `MaterialPlugin` now has a `shadows_enabled` setting, if you didn't spawn the plugin using `::default()` or `..default()`, you'll need to set it. `shadows_enabled: true` is the same behavior as the previous version, and also the default value. --- crates/bevy_pbr/src/material.rs | 21 ++++++++++++++++----- 1 file changed, 16 insertions(+), 5 deletions(-) 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()); From ac49dce4caec320cd65868ef12a8fa520b5686bc Mon Sep 17 00:00:00 2001 From: Arthur Brussee Date: Mon, 18 Mar 2024 17:56:57 +0000 Subject: [PATCH 07/17] Use async-fn in traits rather than BoxedFuture (#12550) # Objective Simplify implementing some asset traits without Box::pin(async move{}) shenanigans. Fixes (in part) https://github.com/bevyengine/bevy/issues/11308 ## Solution Use async-fn in traits when possible in all traits. Traits with return position impl trait are not object safe however, and as AssetReader and AssetWriter are both used with dynamic dispatch, you need a Boxed version of these futures anyway. In the future, Rust is [adding ](https://blog.rust-lang.org/2023/12/21/async-fn-rpit-in-traits.html)proc macros to generate these traits automatically, and at some point in the future dyn traits should 'just work'. Until then.... this seemed liked the right approach given more ErasedXXX already exist, but, no clue if there's plans here! Especially since these are public now, it's a bit of an unfortunate API, and means this is a breaking change. In theory this saves some performance when these traits are used with static dispatch, but, seems like most code paths go through dynamic dispatch, which boxes anyway. I also suspect a bunch of the lifetime annotations on these function could be simplified now as the BoxedFuture was often the only thing returned which needed a lifetime annotation, but I'm not touching that for now as traits + lifetimes can be so tricky. This is a revival of [pull/11362](https://github.com/bevyengine/bevy/pull/11362) after a spectacular merge f*ckup, with updates to the latest Bevy. Just to recap some discussion: - Overall this seems like a win for code quality, especially when implementing these traits, but a loss for having to deal with ErasedXXX variants. - `ConditionalSend` was the preferred name for the trait that might be Send, to deal with wasm platforms. - When reviewing be sure to disable whitespace difference, as that's 95% of the PR. ## Changelog - AssetReader, AssetWriter, AssetLoader, AssetSaver and Process now use async-fn in traits rather than boxed futures. ## Migration Guide - Custom implementations of AssetReader, AssetWriter, AssetLoader, AssetSaver and Process should switch to async fn rather than returning a bevy_utils::BoxedFuture. - Simultaniously, to use dynamic dispatch on these traits you should instead use dyn ErasedXXX. --- crates/bevy_animation/src/graph.rs | 62 ++-- crates/bevy_asset/src/io/android.rs | 69 ++--- crates/bevy_asset/src/io/file/file_asset.rs | 286 ++++++++--------- .../bevy_asset/src/io/file/sync_file_asset.rs | 288 ++++++++---------- crates/bevy_asset/src/io/gated.rs | 35 +-- crates/bevy_asset/src/io/memory.rs | 83 +++-- crates/bevy_asset/src/io/mod.rs | 232 ++++++++++++-- crates/bevy_asset/src/io/processor_gated.rs | 131 ++++---- crates/bevy_asset/src/io/source.rs | 53 ++-- crates/bevy_asset/src/io/wasm.rs | 35 +-- crates/bevy_asset/src/lib.rs | 99 +++--- crates/bevy_asset/src/loader.rs | 4 +- crates/bevy_asset/src/meta.rs | 14 +- crates/bevy_asset/src/processor/mod.rs | 13 +- crates/bevy_asset/src/processor/process.rs | 99 +++--- crates/bevy_asset/src/saver.rs | 6 +- crates/bevy_asset/src/server/loaders.rs | 20 +- crates/bevy_asset/src/server/mod.rs | 10 +- crates/bevy_asset/src/transformer.rs | 4 +- crates/bevy_audio/src/audio_source.rs | 19 +- crates/bevy_gltf/src/loader.rs | 16 +- .../bevy_render/src/render_resource/shader.rs | 64 ++-- .../src/texture/compressed_image_saver.rs | 70 ++--- .../src/texture/exr_texture_loader.rs | 65 ++-- .../src/texture/hdr_texture_loader.rs | 70 +++-- .../bevy_render/src/texture/image_loader.rs | 54 ++-- crates/bevy_scene/src/scene_loader.rs | 29 +- crates/bevy_text/src/font_loader.rs | 16 +- crates/bevy_utils/src/lib.rs | 27 +- examples/asset/asset_decompression.rs | 53 ++-- examples/asset/custom_asset.rs | 39 ++- examples/asset/custom_asset_reader.rs | 35 +-- examples/asset/processing/asset_processing.rs | 89 +++--- 33 files changed, 1092 insertions(+), 1097 deletions(-) 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_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_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_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_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_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/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()) } } From 289a02cad6deefefa1b6195adef6677209f0582f Mon Sep 17 00:00:00 2001 From: Rob Parrett Date: Mon, 18 Mar 2024 11:06:07 -0700 Subject: [PATCH 08/17] bevy_color: Add Tailwind palette (#12080) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit # Objective Give Bevy a well-designed built-in color palette for users to use while prototyping or authoring Bevy examples. ## Solution Generate ([playground](https://play.rust-lang.org/?version=stable&mode=debug&edition=2021&gist=f7b3a3002fb7727db15c1197e0a1a373), [gist](https://gist.github.com/rust-play/f7b3a3002fb7727db15c1197e0a1a373)) consts from [Tailwind](https://tailwindcss.com/docs/customizing-colors) (mit license) json. ## Discussion Are there other popular alternatives we should be looking at? Something new and fancy involving a really long acronym like CIELUVLCh? I'm not a tailwind user or color expert, but I really like the way it's broken up into distinct but plentiful hue and lightness groups. It beats needing some shades of red, scrolling through the [current palette](https://docs.rs/bevy/latest/bevy/prelude/enum.Color.html), choosing a few of `CRIMSON`, `MAROON`, `RED`, `TOMATO` at random and calling it a day. The best information I was able to dig up about the Tailwind palette is from this thread: https://twitter.com/steveschoger/status/1303795136703410180. Here are some key excerpts: > Tried to the "perceptually uniform" thing for Tailwind UI. > Ultimately, it just resulted in a bunch of useless shades for colors like yellow and green that are inherently brighter. > With that said you're guaranteed to get a contrast ratio of 4.5:1 when using any 700 shade (in some cases 600) on a 100 shade of the same hue. > We just spent a lot of time looking at sites to figure out which colors are popular and tried to fill all the gaps. > Even the lime green is questionable but felt there needed to be something in between the jump from yellow to green 😅 --------- Co-authored-by: Alice Cecile --- crates/bevy_color/src/palettes/mod.rs | 1 + crates/bevy_color/src/palettes/tailwind.rs | 536 +++++++++++++++++++++ 2 files changed, 537 insertions(+) create mode 100644 crates/bevy_color/src/palettes/tailwind.rs 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); From 1af9bc853bd7d90d86d4162dcb53ed333d143cc4 Mon Sep 17 00:00:00 2001 From: Pablo Reinhardt <126117294+pablo-lua@users.noreply.github.com> Date: Mon, 18 Mar 2024 15:11:06 -0300 Subject: [PATCH 09/17] Add a gizmo-based overlay to show UI node outlines (Adopted) (#11237) # Objective - This is an adopted version of #10420 - The objective is to help debugging the Ui layout tree with helpful outlines, that can be easily enabled/disabled ## Solution - Like #10420, the solution is using the bevy_gizmos in outlining the nodes --- ## Changelog ### Added - Added debug_overlay mod to `bevy_dev_tools` - Added bevy_ui_debug feature to `bevy_dev_tools` ## How to use - The user must use `bevy_dev_tools` feature in TOML - The user must use the plugin UiDebugPlugin, that can be found on `bevy::dev_tools::debug_overlay` - Finally, to enable the function, the user must set `UiDebugOptions::enabled` to true Someone can easily toggle the function with something like: ```rust fn toggle_overlay(input: Res>, options: ResMut) { if input.just_pressed(KeyCode::Space) { // The toggle method will enable if disabled and disable if enabled options.toggle(); } } ``` Note that this feature can be disabled from dev_tools, as its in fact behind a default feature there, being the feature bevy_ui_debug. # Limitations Currently, due to limitations with gizmos itself, it's not possible to support this feature to more the one window, so this tool is limited to the primary window only. # Showcase ![image](https://github.com/bevyengine/bevy/assets/126117294/ce9d70e6-0a57-4fa9-9753-ff5a9d82c009) Ui example with debug_overlay enabled ![image](https://github.com/bevyengine/bevy/assets/126117294/e945015c-5bab-4d7f-9273-472aabaf25a9) And disabled --------- Co-authored-by: Nicola Papale Co-authored-by: Pablo Reinhardt Co-authored-by: Alice Cecile --- crates/bevy_dev_tools/Cargo.toml | 21 +- .../bevy_dev_tools/src/debug_overlay/inset.rs | 192 ++++++++++++ .../bevy_dev_tools/src/debug_overlay/mod.rs | 280 ++++++++++++++++++ crates/bevy_dev_tools/src/lib.rs | 3 + examples/ui/ui.rs | 58 +++- 5 files changed, 543 insertions(+), 11 deletions(-) create mode 100644 crates/bevy_dev_tools/src/debug_overlay/inset.rs create mode 100644 crates/bevy_dev_tools/src/debug_overlay/mod.rs 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/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/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(); + } +} From 7c7d1e8a6442a4258896b6c605beb1bf50399396 Mon Sep 17 00:00:00 2001 From: "Spencer C. Imbleau" Date: Mon, 18 Mar 2024 20:56:49 -0400 Subject: [PATCH 10/17] refactor: separate out `PanicHandlerPlugin` (#12557) # Objective - Allow configuring of platform-specific panic handlers. - Remove the silent overwrite of the WASM panic handler - Closes #12546 ## Solution - Separates the panic handler to a new plugin, `PanicHandlerPlugin`. - `PanicHandlerPlugin` was added to `DefaultPlugins`. - Can be disabled on `DefaultPlugins`, in the case someone needs to configure custom panic handlers. --- ## Changelog ### Added - A `PanicHandlerPlugin` was added to the `DefaultPlugins`, which now sets sensible target-specific panic handlers. ### Changed - On WASM, the panic stack trace was output to the console through the `BevyLogPlugin`. Since this was separated out into `PanicHandlerPlugin`, you may need to add the new `PanicHandlerPlugin` (included in `DefaultPlugins`). ## Migration Guide - If you used `MinimalPlugins` with `LogPlugin` for a WASM-target build, you will need to add the new `PanicHandlerPlugin` to set the panic behavior to output to the console. Otherwise, you will see the default panic handler (opaque, `unreachable` errors in the console). --- crates/bevy_internal/Cargo.toml | 1 + crates/bevy_internal/src/default_plugins.rs | 2 + crates/bevy_internal/src/lib.rs | 5 ++ crates/bevy_log/Cargo.toml | 1 - crates/bevy_log/src/lib.rs | 1 - crates/bevy_panic_handler/Cargo.toml | 23 +++++++++ crates/bevy_panic_handler/src/lib.rs | 54 +++++++++++++++++++++ 7 files changed, 85 insertions(+), 2 deletions(-) create mode 100644 crates/bevy_panic_handler/Cargo.toml create mode 100644 crates/bevy_panic_handler/src/lib.rs 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_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. + } + } +} From e7a31d000ee7f267ba72615967814bc7c00669fc Mon Sep 17 00:00:00 2001 From: Antony Date: Tue, 19 Mar 2024 18:44:00 -0400 Subject: [PATCH 11/17] Add border radius to UI nodes (adopted) (#12500) # Objective Implements border radius for UI nodes. Adopted from #8973, but excludes shadows. ## Solution - Add a component `BorderRadius` which contains a radius value for each corner of the UI node. - Use a fragment shader to generate the rounded corners using a signed distance function. ## Changelog - `BorderRadius`: New component that holds the border radius values. - `NodeBundle` & `ButtonBundle`: Added a `border_radius: BorderRadius` field. - `extract_uinode_borders`: Stripped down, most of the work is done in the shader now. Borders are no longer assembled from multiple rects, instead the shader uses a signed distance function to draw the border. - `UiVertex`: Added size, border and radius fields. - `UiPipeline`: Added three vertex attributes to the vertex buffer layout, to accept the UI node's size, border thickness and border radius. - Examples: Added rounded corners to the UI element in the `button` example, and a `rounded_borders` example. --------- Co-authored-by: Alice Cecile Co-authored-by: Zachary Harrold Co-authored-by: Pablo Reinhardt <126117294+pablo-lua@users.noreply.github.com> --- Cargo.toml | 11 + crates/bevy_ui/src/lib.rs | 1 + crates/bevy_ui/src/node_bundles.rs | 10 +- crates/bevy_ui/src/render/mod.rs | 285 ++++++++++++++++------ crates/bevy_ui/src/render/pipeline.rs | 6 + crates/bevy_ui/src/render/render_pass.rs | 13 +- crates/bevy_ui/src/render/ui.wgsl | 298 ++++++++++++++++++++++- crates/bevy_ui/src/texture_slice.rs | 5 +- crates/bevy_ui/src/ui_node.rs | 262 ++++++++++++++++++++ examples/README.md | 1 + examples/ui/button.rs | 1 + examples/ui/rounded_borders.rs | 176 +++++++++++++ 12 files changed, 977 insertions(+), 92 deletions(-) create mode 100644 examples/ui/rounded_borders.rs 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_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..2ade33e359a52d 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, + &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,9 @@ pub fn extract_uinode_background_colors( continue; } + let border_radius = + resolve_border_radius(border_radius, uinode.size(), viewport_size, ui_scale.0); + extracted_uinodes.uinodes.insert( entity, ExtractedUiNode { @@ -206,6 +243,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 +254,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 +268,19 @@ pub fn extract_uinode_images( &UiImage, Option<&TextureAtlas>, Option<&ComputedTextureSlices>, + &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 +323,9 @@ pub fn extract_uinode_images( ), }; + let border_radius = + resolve_border_radius(border_radius, uinode.size(), viewport_size, ui_scale.0); + extracted_uinodes.uinodes.insert( commands.spawn_empty().id(), ExtractedUiNode { @@ -285,6 +339,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 +359,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 +425,7 @@ pub fn extract_uinode_borders( Option<&Parent>, &Style, &BorderColor, + &BorderRadius, ), Without, >, @@ -327,8 +434,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 +484,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 +586,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 +605,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 +778,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 +793,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 +817,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 +839,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 +921,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 +954,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 +1004,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 +1068,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 +1108,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 +1139,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/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/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/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); + } +} From d7372f2c7548a8f63bcd3f338bbf19b02946c1a8 Mon Sep 17 00:00:00 2001 From: Lynn <62256001+solis-lumine-vorago@users.noreply.github.com> Date: Tue, 19 Mar 2024 23:46:33 +0100 Subject: [PATCH 12/17] Color maths 4 (#12575) # Objective - Fixes #12202 ## Solution - This PR implements componentwise (including alpha) addition, subtraction and scalar multiplication/division for some color types. - The mentioned color types are `Laba`, `Oklaba`, `LinearRgba` and `Xyza` as all of them are either physically or perceptually linear as mentioned by @alice-i-cecile in the issue. --- ## Changelog - Scalar mul/div for `LinearRgba` may modify alpha now. ## Migration Guide - Users of scalar mul/div for `LinearRgba` need to be aware of the change and maybe use the `.clamp()` methods or manually set the `alpha` channel. --- crates/bevy_color/src/laba.rs | 6 ++- crates/bevy_color/src/lib.rs | 64 ++++++++++++++++++++++++++++ crates/bevy_color/src/linear_rgba.rs | 49 ++------------------- crates/bevy_color/src/oklaba.rs | 6 ++- crates/bevy_color/src/xyza.rs | 6 ++- 5 files changed, 81 insertions(+), 50 deletions(-) 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..4da4c805fb97cd 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}; @@ -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. /// 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. /// From 40f82b867b2a21ed919f7bf921a62d4c1ddd8874 Mon Sep 17 00:00:00 2001 From: Pablo Reinhardt <126117294+pablo-lua@users.noreply.github.com> Date: Tue, 19 Mar 2024 19:50:17 -0300 Subject: [PATCH 13/17] Reflect default in some types on `bevy_render` (#12580) # Objective - Many types in bevy_render doesn't reflect Default even if it could. ## Solution - Reflect it. --- --------- Co-authored-by: Pablo Reinhardt --- crates/bevy_render/src/camera/camera.rs | 6 +++--- crates/bevy_render/src/camera/clear_color.rs | 6 +++--- crates/bevy_render/src/globals.rs | 4 ++-- crates/bevy_render/src/mesh/mesh/skinning.rs | 4 ++-- crates/bevy_render/src/mesh/morph.rs | 6 +++--- crates/bevy_render/src/primitives/mod.rs | 10 +++++----- crates/bevy_render/src/texture/image.rs | 4 ++-- crates/bevy_render/src/view/mod.rs | 2 +- crates/bevy_render/src/view/visibility/mod.rs | 2 +- 9 files changed, 22 insertions(+), 22 deletions(-) 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/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/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, From f38895a414f605578d81a6d99a58e1478767e0a5 Mon Sep 17 00:00:00 2001 From: Antony Date: Tue, 19 Mar 2024 18:50:42 -0400 Subject: [PATCH 14/17] Fix `Oklab` and `Oklch` color space inconsistency (#12583) # Objective Fixes #12224. ## Solution - Expand `with_` methods for the `Oklch` to their full names. - Expand `l` to `lightness` in `Oklaba` comments. ## Migration Guide The following methods have been renamed for the `Oklch` color space: - `with_l` -> `with_lightness`. - `with_c` -> `with_chroma`. - `with_h` -> `with_hue`. --- crates/bevy_color/src/oklaba.rs | 4 ++-- crates/bevy_color/src/oklcha.rs | 6 +++--- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/crates/bevy_color/src/oklaba.rs b/crates/bevy_color/src/oklaba.rs index 4da4c805fb97cd..5501e8e4c6ec8b 100644 --- a/crates/bevy_color/src/oklaba.rs +++ b/crates/bevy_color/src/oklaba.rs @@ -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, @@ -61,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 } } From 779e4c4901d877a1d02f339588cd51a13dd4cd57 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fran=C3=A7ois=20Mockers?= Date: Wed, 20 Mar 2024 14:11:24 +0100 Subject: [PATCH 15/17] UI: allow border radius to be optional for images and background (#12592) # Objective - #12500 broke images and background colors in UI. Try examples `overflow`, `ui_scaling` or `ui_texture_atlas` ## Solution - Makes the component `BorderRadius` optional in the query, as it's not always present. Use `[0.; 4]` as border radius in the extracted node when none was found --- crates/bevy_ui/src/render/mod.rs | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/crates/bevy_ui/src/render/mod.rs b/crates/bevy_ui/src/render/mod.rs index 2ade33e359a52d..882a87ba484b55 100644 --- a/crates/bevy_ui/src/render/mod.rs +++ b/crates/bevy_ui/src/render/mod.rs @@ -193,7 +193,7 @@ pub fn extract_uinode_background_colors( Option<&CalculatedClip>, Option<&TargetCamera>, &BackgroundColor, - &BorderRadius, + Option<&BorderRadius>, )>, >, ) { @@ -224,8 +224,11 @@ pub fn extract_uinode_background_colors( continue; } - let border_radius = - resolve_border_radius(border_radius, uinode.size(), viewport_size, ui_scale.0); + 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, @@ -268,7 +271,7 @@ pub fn extract_uinode_images( &UiImage, Option<&TextureAtlas>, Option<&ComputedTextureSlices>, - &BorderRadius, + Option<&BorderRadius>, )>, >, ) { @@ -323,8 +326,11 @@ pub fn extract_uinode_images( ), }; - let border_radius = - resolve_border_radius(border_radius, uinode.size(), viewport_size, ui_scale.0); + 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(), From 4d0d070059170398d888ba6309c0926ea42abf26 Mon Sep 17 00:00:00 2001 From: IceSentry Date: Wed, 20 Mar 2024 09:11:48 -0400 Subject: [PATCH 16/17] Always spawn fps_overlay on top of everything (#12586) # Objective - Currently the fps_overlay affects any other ui node spawned. This should not happen ## Solution - Use position absolute and a ZIndex of `i32::MAX - 32` - I also modified the example a little bit to center it correctly. It only worked previously because the overlay was pushing it down. I also took the opportunity to simplify the text spawning code a little bit. --- crates/bevy_dev_tools/src/fps_overlay.rs | 39 +++++++++++++++++----- examples/dev_tools/fps_overlay.rs | 41 +++++++++++++----------- 2 files changed, 53 insertions(+), 27 deletions(-) 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/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) { From ed44eb3913b1180604d468f1ae88fcd2dd3f68a5 Mon Sep 17 00:00:00 2001 From: Brezak Date: Wed, 20 Mar 2024 15:21:50 +0100 Subject: [PATCH 17/17] Add a from Dir2 impl for Vec2 (#12594) # Objective Allow converting from `Dir2` to `Vec2` in generic code. Fixes #12529. ## Solution Added a `From` impl for `Vec2`. --- crates/bevy_math/src/direction.rs | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/crates/bevy_math/src/direction.rs b/crates/bevy_math/src/direction.rs index a88d44b6024a4b..150a9105b86e1d 100644 --- a/crates/bevy_math/src/direction.rs +++ b/crates/bevy_math/src/direction.rs @@ -151,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 {