Merge branch 'main' into render-diagnostics

This commit is contained in:
Lucas Farias 2025-07-02 18:08:33 -03:00
commit cb159ef5b6
280 changed files with 12406 additions and 3959 deletions

View File

@ -15,7 +15,7 @@ env:
# If nightly is breaking CI, modify this variable to target a specific nightly version.
NIGHTLY_TOOLCHAIN: nightly
RUSTFLAGS: "-D warnings"
BINSTALL_VERSION: "v1.12.5"
BINSTALL_VERSION: "v1.14.1"
concurrency:
group: ${{github.workflow}}-${{github.ref}}
@ -260,7 +260,7 @@ jobs:
# Full git history is needed to get a proper list of changed files within `super-linter`
fetch-depth: 0
- name: Run Markdown Lint
uses: super-linter/super-linter/slim@v7.3.0
uses: super-linter/super-linter/slim@v7.4.0
env:
MULTI_STATUS: false
VALIDATE_ALL_CODEBASE: false
@ -272,7 +272,8 @@ jobs:
timeout-minutes: 30
steps:
- uses: actions/checkout@v4
- uses: cargo-bins/cargo-binstall@v1.12.5
# Update in sync with BINSTALL_VERSION
- uses: cargo-bins/cargo-binstall@v1.14.1
- name: Install taplo
run: cargo binstall taplo-cli@0.9.3 --locked
- name: Run Taplo

View File

@ -306,6 +306,9 @@ bevy_input_focus = ["bevy_internal/bevy_input_focus"]
# Headless widget collection for Bevy UI.
bevy_core_widgets = ["bevy_internal/bevy_core_widgets"]
# Feathers widget collection.
experimental_bevy_feathers = ["bevy_internal/bevy_feathers"]
# Enable passthrough loading for SPIR-V shaders (Only supported on Vulkan, shader capabilities and extensions must agree with the platform implementation)
spirv_shader_passthrough = ["bevy_internal/spirv_shader_passthrough"]
@ -484,6 +487,12 @@ shader_format_wesl = ["bevy_internal/shader_format_wesl"]
# Enable support for transmission-related textures in the `StandardMaterial`, at the risk of blowing past the global, per-shader texture limit on older/lower-end GPUs
pbr_transmission_textures = ["bevy_internal/pbr_transmission_textures"]
# Enable support for Clustered Decals
pbr_clustered_decals = ["bevy_internal/pbr_clustered_decals"]
# Enable support for Light Textures
pbr_light_textures = ["bevy_internal/pbr_light_textures"]
# Enable support for multi-layer material textures in the `StandardMaterial`, at the risk of blowing past the global, per-shader texture limit on older/lower-end GPUs
pbr_multi_layer_material_textures = [
"bevy_internal/pbr_multi_layer_material_textures",
@ -561,6 +570,11 @@ web = ["bevy_internal/web"]
# Enable hotpatching of Bevy systems
hotpatching = ["bevy_internal/hotpatching"]
# Enable converting glTF coordinates to Bevy's coordinate system by default. This will be Bevy's default behavior starting in 0.18.
gltf_convert_coordinates_default = [
"bevy_internal/gltf_convert_coordinates_default",
]
# Enable collecting debug information about systems and components to help with diagnostics
debug = ["bevy_internal/debug"]
@ -579,7 +593,7 @@ ron = "0.10"
flate2 = "1.0"
serde = { version = "1", features = ["derive"] }
serde_json = "1.0.140"
bytemuck = "1.7"
bytemuck = "1"
bevy_render = { path = "crates/bevy_render", version = "0.17.0-dev", default-features = false }
# The following explicit dependencies are needed for proc macros to work inside of examples as they are part of the bevy crate itself.
bevy_ecs = { path = "crates/bevy_ecs", version = "0.17.0-dev", default-features = false }
@ -1051,6 +1065,17 @@ description = "Showcases different blend modes"
category = "3D Rendering"
wasm = true
[[example]]
name = "manual_material"
path = "examples/3d/manual_material.rs"
doc-scrape-examples = true
[package.metadata.example.manual_material]
name = "Manual Material Implementation"
description = "Demonstrates how to implement a material manually using the mid-level render APIs"
category = "3D Rendering"
wasm = true
[[example]]
name = "edit_material_on_gltf"
path = "examples/3d/edit_material_on_gltf.rs"
@ -4401,6 +4426,7 @@ wasm = true
name = "clustered_decals"
path = "examples/3d/clustered_decals.rs"
doc-scrape-examples = true
required-features = ["pbr_clustered_decals"]
[package.metadata.example.clustered_decals]
name = "Clustered Decals"
@ -4408,6 +4434,18 @@ description = "Demonstrates clustered decals"
category = "3D Rendering"
wasm = false
[[example]]
name = "light_textures"
path = "examples/3d/light_textures.rs"
doc-scrape-examples = true
required-features = ["pbr_light_textures"]
[package.metadata.example.light_textures]
name = "Light Textures"
description = "Demonstrates light textures"
category = "3D Rendering"
wasm = false
[[example]]
name = "occlusion_culling"
path = "examples/3d/occlusion_culling.rs"
@ -4510,3 +4548,27 @@ name = "Core Widgets (w/Observers)"
description = "Demonstrates use of core (headless) widgets in Bevy UI, with Observers"
category = "UI (User Interface)"
wasm = true
[[example]]
name = "scrollbars"
path = "examples/ui/scrollbars.rs"
doc-scrape-examples = true
[package.metadata.example.scrollbars]
name = "Scrollbars"
description = "Demonstrates use of core scrollbar in Bevy UI"
category = "UI (User Interface)"
wasm = true
[[example]]
name = "feathers"
path = "examples/ui/feathers.rs"
doc-scrape-examples = true
required-features = ["experimental_bevy_feathers"]
[package.metadata.example.feathers]
name = "Feathers Widgets"
description = "Gallery of Feathers Widgets"
category = "UI (User Interface)"
wasm = true
hidden = true

View File

@ -9,20 +9,20 @@
(
node_type: Blend,
mask: 0,
weight: 1.0,
weight: 0.5,
),
(
node_type: Clip(AssetPath("models/animated/Fox.glb#Animation0")),
node_type: Clip("models/animated/Fox.glb#Animation0"),
mask: 0,
weight: 1.0,
),
(
node_type: Clip(AssetPath("models/animated/Fox.glb#Animation1")),
node_type: Clip("models/animated/Fox.glb#Animation1"),
mask: 0,
weight: 1.0,
),
(
node_type: Clip(AssetPath("models/animated/Fox.glb#Animation2")),
node_type: Clip("models/animated/Fox.glb#Animation2"),
mask: 0,
weight: 1.0,
),

Binary file not shown.

After

Width:  |  Height:  |  Size: 229 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 551 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 93 KiB

Binary file not shown.

View File

@ -0,0 +1,11 @@
#import bevy_pbr::forward_io::VertexOutput
@group(3) @binding(0) var material_color_texture: texture_2d<f32>;
@group(3) @binding(1) var material_color_sampler: sampler;
@fragment
fn fragment(
mesh: VertexOutput,
) -> @location(0) vec4<f32> {
return textureSample(material_color_texture, material_color_sampler, mesh.uv);
}

View File

@ -10,7 +10,7 @@ autobenches = false
[dependencies]
# The primary crate that runs and analyzes our benchmarks. This is a regular dependency because the
# `bench!` macro refers to it in its documentation.
criterion = { version = "0.5.1", features = ["html_reports"] }
criterion = { version = "0.6.0", features = ["html_reports"] }
[dev-dependencies]
# Bevy crates

View File

@ -0,0 +1,67 @@
use benches::bench;
use bevy_ecs::{component::Component, world::World};
use criterion::Criterion;
const ENTITY_COUNT: usize = 2_000;
#[derive(Component)]
struct C<const N: usize>(usize);
pub fn insert_many(criterion: &mut Criterion) {
let mut group = criterion.benchmark_group(bench!("insert_many"));
group.bench_function("all", |bencher| {
let mut world = World::new();
bencher.iter(|| {
for _ in 0..ENTITY_COUNT {
world
.spawn_empty()
.insert(C::<0>(1))
.insert(C::<1>(1))
.insert(C::<2>(1))
.insert(C::<3>(1))
.insert(C::<4>(1))
.insert(C::<5>(1))
.insert(C::<6>(1))
.insert(C::<7>(1))
.insert(C::<8>(1))
.insert(C::<9>(1))
.insert(C::<10>(1))
.insert(C::<11>(1))
.insert(C::<12>(1))
.insert(C::<13>(1))
.insert(C::<14>(1));
}
world.clear_entities();
});
});
group.bench_function("only_last", |bencher| {
let mut world = World::new();
bencher.iter(|| {
for _ in 0..ENTITY_COUNT {
world
.spawn((
C::<0>(1),
C::<1>(1),
C::<2>(1),
C::<3>(1),
C::<4>(1),
C::<5>(1),
C::<6>(1),
C::<7>(1),
C::<8>(1),
C::<9>(1),
C::<10>(1),
C::<11>(1),
C::<12>(1),
C::<13>(1),
))
.insert(C::<14>(1));
}
world.clear_entities();
});
});
group.finish();
}

View File

@ -0,0 +1,14 @@
use criterion::criterion_group;
mod insert_many;
mod spawn_many;
mod spawn_many_zst;
mod spawn_one_zst;
criterion_group!(
benches,
spawn_one_zst::spawn_one_zst,
spawn_many_zst::spawn_many_zst,
spawn_many::spawn_many,
insert_many::insert_many,
);

View File

@ -0,0 +1,40 @@
use benches::bench;
use bevy_ecs::{component::Component, world::World};
use criterion::Criterion;
const ENTITY_COUNT: usize = 2_000;
#[derive(Component)]
struct C<const N: usize>(usize);
pub fn spawn_many(criterion: &mut Criterion) {
let mut group = criterion.benchmark_group(bench!("spawn_many"));
group.bench_function("static", |bencher| {
let mut world = World::new();
bencher.iter(|| {
for _ in 0..ENTITY_COUNT {
world.spawn((
C::<0>(1),
C::<1>(1),
C::<2>(1),
C::<3>(1),
C::<4>(1),
C::<5>(1),
C::<6>(1),
C::<7>(1),
C::<8>(1),
C::<9>(1),
C::<10>(1),
C::<11>(1),
C::<12>(1),
C::<13>(1),
C::<14>(1),
));
}
world.clear_entities();
});
});
group.finish();
}

View File

@ -0,0 +1,27 @@
use benches::bench;
use bevy_ecs::{component::Component, world::World};
use criterion::Criterion;
const ENTITY_COUNT: usize = 2_000;
#[derive(Component)]
struct C<const N: usize>;
pub fn spawn_many_zst(criterion: &mut Criterion) {
let mut group = criterion.benchmark_group(bench!("spawn_many_zst"));
group.bench_function("static", |bencher| {
let mut world = World::new();
bencher.iter(|| {
for _ in 0..ENTITY_COUNT {
world.spawn((
C::<0>, C::<1>, C::<2>, C::<3>, C::<4>, C::<5>, C::<6>, C::<7>, C::<8>, C::<9>,
C::<10>, C::<11>, C::<12>, C::<13>, C::<14>,
));
}
world.clear_entities();
});
});
group.finish();
}

View File

@ -0,0 +1,24 @@
use benches::bench;
use bevy_ecs::{component::Component, world::World};
use criterion::Criterion;
const ENTITY_COUNT: usize = 10_000;
#[derive(Component)]
struct A;
pub fn spawn_one_zst(criterion: &mut Criterion) {
let mut group = criterion.benchmark_group(bench!("spawn_one_zst"));
group.bench_function("static", |bencher| {
let mut world = World::new();
bencher.iter(|| {
for _ in 0..ENTITY_COUNT {
world.spawn(A);
}
world.clear_entities();
});
});
group.finish();
}

View File

@ -5,6 +5,7 @@
use criterion::criterion_main;
mod bundles;
mod change_detection;
mod components;
mod empty_archetypes;
@ -18,6 +19,7 @@ mod scheduling;
mod world;
criterion_main!(
bundles::benches,
change_detection::benches,
components::benches,
empty_archetypes::benches,

View File

@ -31,7 +31,7 @@ bevy_platform = { path = "../bevy_platform", version = "0.17.0-dev", default-fea
] }
# other
petgraph = { version = "0.7", features = ["serde-1"] }
petgraph = { version = "0.8", features = ["serde-1"] }
ron = "0.10"
serde = "1"
blake3 = { version = "1.0" }

View File

@ -19,7 +19,7 @@ use bevy_ecs::{
system::{Res, ResMut},
};
use bevy_platform::collections::HashMap;
use bevy_reflect::{prelude::ReflectDefault, Reflect, ReflectSerialize};
use bevy_reflect::{prelude::ReflectDefault, Reflect};
use derive_more::derive::From;
use petgraph::{
graph::{DiGraph, NodeIndex},
@ -29,6 +29,7 @@ use ron::de::SpannedError;
use serde::{Deserialize, Serialize};
use smallvec::SmallVec;
use thiserror::Error;
use tracing::warn;
use crate::{AnimationClip, AnimationTargetId};
@ -108,9 +109,8 @@ use crate::{AnimationClip, AnimationTargetId};
/// [RON]: https://github.com/ron-rs/ron
///
/// [RFC 51]: https://github.com/bevyengine/rfcs/blob/main/rfcs/51-animation-composition.md
#[derive(Asset, Reflect, Clone, Debug, Serialize)]
#[reflect(Serialize, Debug, Clone)]
#[serde(into = "SerializedAnimationGraph")]
#[derive(Asset, Reflect, Clone, Debug)]
#[reflect(Debug, Clone)]
pub struct AnimationGraph {
/// The `petgraph` data structure that defines the animation graph.
pub graph: AnimationDiGraph,
@ -242,20 +242,40 @@ pub enum AnimationNodeType {
#[derive(Default)]
pub struct AnimationGraphAssetLoader;
/// Various errors that can occur when serializing or deserializing animation
/// graphs to and from RON, respectively.
/// Errors that can occur when serializing animation graphs to RON.
#[derive(Error, Debug)]
pub enum AnimationGraphSaveError {
/// An I/O error occurred.
#[error(transparent)]
Io(#[from] io::Error),
/// An error occurred in RON serialization.
#[error(transparent)]
Ron(#[from] ron::Error),
/// An error occurred converting the graph to its serialization form.
#[error(transparent)]
ConvertToSerialized(#[from] NonPathHandleError),
}
/// Errors that can occur when deserializing animation graphs from RON.
#[derive(Error, Debug)]
pub enum AnimationGraphLoadError {
/// An I/O error occurred.
#[error("I/O")]
#[error(transparent)]
Io(#[from] io::Error),
/// An error occurred in RON serialization or deserialization.
#[error("RON serialization")]
/// An error occurred in RON deserialization.
#[error(transparent)]
Ron(#[from] ron::Error),
/// An error occurred in RON deserialization, and the location of the error
/// is supplied.
#[error("RON serialization")]
#[error(transparent)]
SpannedRon(#[from] SpannedError),
/// The deserialized graph contained legacy data that we no longer support.
#[error(
"The deserialized AnimationGraph contained an AnimationClip referenced by an AssetId, \
which is no longer supported. Consider manually deserializing the SerializedAnimationGraph \
type and determine how to migrate any SerializedAnimationClip::AssetId animation clips"
)]
GraphContainsLegacyAssetId,
}
/// Acceleration structures for animation graphs that allows Bevy to evaluate
@ -388,18 +408,32 @@ pub struct SerializedAnimationGraphNode {
#[derive(Serialize, Deserialize)]
pub enum SerializedAnimationNodeType {
/// Corresponds to [`AnimationNodeType::Clip`].
Clip(SerializedAnimationClip),
Clip(MigrationSerializedAnimationClip),
/// Corresponds to [`AnimationNodeType::Blend`].
Blend,
/// Corresponds to [`AnimationNodeType::Add`].
Add,
}
/// A version of `Handle<AnimationClip>` suitable for serializing as an asset.
/// A type to facilitate migration from the legacy format of [`SerializedAnimationGraph`] to the
/// new format.
///
/// This replaces any handle that has a path with an [`AssetPath`]. Failing
/// that, the asset ID is serialized directly.
/// By using untagged serde deserialization, we can try to deserialize the modern form, then
/// fallback to the legacy form. Users must migrate to the modern form by Bevy 0.18.
// TODO: Delete this after Bevy 0.17.
#[derive(Serialize, Deserialize)]
#[serde(untagged)]
pub enum MigrationSerializedAnimationClip {
/// This is the new type of this field.
Modern(AssetPath<'static>),
/// This is the legacy type of this field. Users must migrate away from this.
#[serde(skip_serializing)]
Legacy(SerializedAnimationClip),
}
/// The legacy form of serialized animation clips. This allows raw asset IDs to be deserialized.
// TODO: Delete this after Bevy 0.17.
#[derive(Deserialize)]
pub enum SerializedAnimationClip {
/// Records an asset path.
AssetPath(AssetPath<'static>),
@ -648,12 +682,13 @@ impl AnimationGraph {
///
/// If writing to a file, it can later be loaded with the
/// [`AnimationGraphAssetLoader`] to reconstruct the graph.
pub fn save<W>(&self, writer: &mut W) -> Result<(), AnimationGraphLoadError>
pub fn save<W>(&self, writer: &mut W) -> Result<(), AnimationGraphSaveError>
where
W: Write,
{
let mut ron_serializer = ron::ser::Serializer::new(writer, None)?;
Ok(self.serialize(&mut ron_serializer)?)
let serialized_graph: SerializedAnimationGraph = self.clone().try_into()?;
Ok(serialized_graph.serialize(&mut ron_serializer)?)
}
/// Adds an animation target (bone) to the mask group with the given ID.
@ -758,28 +793,55 @@ impl AssetLoader for AnimationGraphAssetLoader {
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 {
node_type: match serialized_node.node_type {
SerializedAnimationNodeType::Clip(ref clip) => match clip {
SerializedAnimationClip::AssetId(asset_id) => {
AnimationNodeType::Clip(Handle::Weak(*asset_id))
// Load all `AssetPath`s to convert from a `SerializedAnimationGraph` to a real
// `AnimationGraph`. This is effectively a `DiGraph::map`, but this allows us to return
// errors.
let mut animation_graph = DiGraph::with_capacity(
serialized_animation_graph.graph.node_count(),
serialized_animation_graph.graph.edge_count(),
);
let mut already_warned = false;
for serialized_node in serialized_animation_graph.graph.node_weights() {
animation_graph.add_node(AnimationGraphNode {
node_type: match serialized_node.node_type {
SerializedAnimationNodeType::Clip(ref clip) => match clip {
MigrationSerializedAnimationClip::Modern(path) => {
AnimationNodeType::Clip(load_context.load(path.clone()))
}
MigrationSerializedAnimationClip::Legacy(
SerializedAnimationClip::AssetPath(path),
) => {
if !already_warned {
let path = load_context.asset_path();
warn!(
"Loaded an AnimationGraph asset at \"{path}\" which contains a \
legacy-style SerializedAnimationClip. Please re-save the asset \
using AnimationGraph::save to automatically migrate to the new \
format"
);
already_warned = true;
}
SerializedAnimationClip::AssetPath(asset_path) => {
AnimationNodeType::Clip(load_context.load(asset_path))
}
},
SerializedAnimationNodeType::Blend => AnimationNodeType::Blend,
SerializedAnimationNodeType::Add => AnimationNodeType::Add,
AnimationNodeType::Clip(load_context.load(path.clone()))
}
MigrationSerializedAnimationClip::Legacy(
SerializedAnimationClip::AssetId(_),
) => {
return Err(AnimationGraphLoadError::GraphContainsLegacyAssetId);
}
},
mask: serialized_node.mask,
weight: serialized_node.weight,
SerializedAnimationNodeType::Blend => AnimationNodeType::Blend,
SerializedAnimationNodeType::Add => AnimationNodeType::Add,
},
|_, _| (),
),
mask: serialized_node.mask,
weight: serialized_node.weight,
});
}
for edge in serialized_animation_graph.graph.raw_edges() {
animation_graph.add_edge(edge.source(), edge.target(), ());
}
Ok(AnimationGraph {
graph: animation_graph,
root: serialized_animation_graph.root,
mask_groups: serialized_animation_graph.mask_groups,
})
@ -790,37 +852,50 @@ impl AssetLoader for AnimationGraphAssetLoader {
}
}
impl From<AnimationGraph> for SerializedAnimationGraph {
fn from(animation_graph: AnimationGraph) -> Self {
// If any of the animation clips have paths, then serialize them as
// `SerializedAnimationClip::AssetPath` so that the
// `AnimationGraphAssetLoader` can load them.
Self {
graph: animation_graph.graph.map(
|_, node| SerializedAnimationGraphNode {
weight: node.weight,
mask: node.mask,
node_type: match node.node_type {
AnimationNodeType::Clip(ref clip) => match clip.path() {
Some(path) => SerializedAnimationNodeType::Clip(
SerializedAnimationClip::AssetPath(path.clone()),
),
None => SerializedAnimationNodeType::Clip(
SerializedAnimationClip::AssetId(clip.id()),
),
},
AnimationNodeType::Blend => SerializedAnimationNodeType::Blend,
AnimationNodeType::Add => SerializedAnimationNodeType::Add,
impl TryFrom<AnimationGraph> for SerializedAnimationGraph {
type Error = NonPathHandleError;
fn try_from(animation_graph: AnimationGraph) -> Result<Self, NonPathHandleError> {
// Convert all the `Handle<AnimationClip>` to AssetPath, so that
// `AnimationGraphAssetLoader` can load them. This is effectively just doing a
// `DiGraph::map`, except we need to return an error if any handles aren't associated to a
// path.
let mut serialized_graph = DiGraph::with_capacity(
animation_graph.graph.node_count(),
animation_graph.graph.edge_count(),
);
for node in animation_graph.graph.node_weights() {
serialized_graph.add_node(SerializedAnimationGraphNode {
weight: node.weight,
mask: node.mask,
node_type: match node.node_type {
AnimationNodeType::Clip(ref clip) => match clip.path() {
Some(path) => SerializedAnimationNodeType::Clip(
MigrationSerializedAnimationClip::Modern(path.clone()),
),
None => return Err(NonPathHandleError),
},
AnimationNodeType::Blend => SerializedAnimationNodeType::Blend,
AnimationNodeType::Add => SerializedAnimationNodeType::Add,
},
|_, _| (),
),
});
}
for edge in animation_graph.graph.raw_edges() {
serialized_graph.add_edge(edge.source(), edge.target(), ());
}
Ok(Self {
graph: serialized_graph,
root: animation_graph.root,
mask_groups: animation_graph.mask_groups,
}
})
}
}
/// Error for when only path [`Handle`]s are supported.
#[derive(Error, Debug)]
#[error("AnimationGraph contains a handle to an AnimationClip that does not correspond to an asset path")]
pub struct NonPathHandleError;
/// A system that creates, updates, and removes [`ThreadedAnimationGraph`]
/// structures for every changed [`AnimationGraph`].
///

View File

@ -11,7 +11,7 @@ use bevy_reflect::{std_traits::ReflectDefault, Reflect};
use bevy_render::{
extract_component::{ExtractComponent, ExtractComponentPlugin, UniformComponentPlugin},
prelude::Camera,
render_graph::RenderGraphApp,
render_graph::RenderGraphExt,
render_resource::{
binding_types::{sampler, texture_2d, uniform_buffer},
*,
@ -20,6 +20,7 @@ use bevy_render::{
view::{ExtractedView, ViewTarget},
Render, RenderApp, RenderSystems,
};
use bevy_utils::default;
mod node;
@ -218,18 +219,14 @@ impl SpecializedRenderPipeline for CasPipeline {
fragment: Some(FragmentState {
shader: self.fragment_shader.clone(),
shader_defs,
entry_point: "fragment".into(),
targets: vec![Some(ColorTargetState {
format: key.texture_format,
blend: None,
write_mask: ColorWrites::ALL,
})],
..default()
}),
primitive: PrimitiveState::default(),
depth_stencil: None,
multisample: MultisampleState::default(),
push_constant_ranges: Vec::new(),
zero_initialize_workgroup_memory: false,
..default()
}
}
}

View File

@ -11,7 +11,7 @@ use bevy_reflect::{std_traits::ReflectDefault, Reflect};
use bevy_render::{
extract_component::{ExtractComponent, ExtractComponentPlugin},
prelude::Camera,
render_graph::{RenderGraphApp, ViewNodeRunner},
render_graph::{RenderGraphExt, ViewNodeRunner},
render_resource::{
binding_types::{sampler, texture_2d},
*,
@ -190,18 +190,14 @@ impl SpecializedRenderPipeline for FxaaPipeline {
format!("EDGE_THRESH_{}", key.edge_threshold.get_str()).into(),
format!("EDGE_THRESH_MIN_{}", key.edge_threshold_min.get_str()).into(),
],
entry_point: "fragment".into(),
targets: vec![Some(ColorTargetState {
format: key.texture_format,
blend: None,
write_mask: ColorWrites::ALL,
})],
..default()
}),
primitive: PrimitiveState::default(),
depth_stencil: None,
multisample: MultisampleState::default(),
push_constant_ranges: Vec::new(),
zero_initialize_workgroup_memory: false,
..default()
}
}
}

View File

@ -32,7 +32,7 @@
use bevy_app::{App, Plugin};
#[cfg(feature = "smaa_luts")]
use bevy_asset::load_internal_binary_asset;
use bevy_asset::{embedded_asset, load_embedded_asset, weak_handle, Handle};
use bevy_asset::{embedded_asset, load_embedded_asset, uuid_handle, Handle};
#[cfg(not(feature = "smaa_luts"))]
use bevy_core_pipeline::tonemapping::lut_placeholder;
use bevy_core_pipeline::{
@ -58,19 +58,19 @@ use bevy_render::{
extract_component::{ExtractComponent, ExtractComponentPlugin},
render_asset::RenderAssets,
render_graph::{
NodeRunError, RenderGraphApp as _, RenderGraphContext, ViewNode, ViewNodeRunner,
NodeRunError, RenderGraphContext, RenderGraphExt as _, ViewNode, ViewNodeRunner,
},
render_resource::{
binding_types::{sampler, texture_2d, uniform_buffer},
AddressMode, BindGroup, BindGroupEntries, BindGroupLayout, BindGroupLayoutEntries,
CachedRenderPipelineId, ColorTargetState, ColorWrites, CompareFunction, DepthStencilState,
DynamicUniformBuffer, FilterMode, FragmentState, LoadOp, MultisampleState, Operations,
PipelineCache, PrimitiveState, RenderPassColorAttachment, RenderPassDepthStencilAttachment,
RenderPassDescriptor, RenderPipeline, RenderPipelineDescriptor, SamplerBindingType,
SamplerDescriptor, Shader, ShaderDefVal, ShaderStages, ShaderType,
SpecializedRenderPipeline, SpecializedRenderPipelines, StencilFaceState, StencilOperation,
StencilState, StoreOp, TextureDescriptor, TextureDimension, TextureFormat,
TextureSampleType, TextureUsages, TextureView, VertexState,
DynamicUniformBuffer, FilterMode, FragmentState, LoadOp, Operations, PipelineCache,
RenderPassColorAttachment, RenderPassDepthStencilAttachment, RenderPassDescriptor,
RenderPipeline, RenderPipelineDescriptor, SamplerBindingType, SamplerDescriptor, Shader,
ShaderDefVal, ShaderStages, ShaderType, SpecializedRenderPipeline,
SpecializedRenderPipelines, StencilFaceState, StencilOperation, StencilState, StoreOp,
TextureDescriptor, TextureDimension, TextureFormat, TextureSampleType, TextureUsages,
TextureView, VertexState,
},
renderer::{RenderContext, RenderDevice, RenderQueue},
texture::{CachedTexture, GpuImage, TextureCache},
@ -81,10 +81,10 @@ use bevy_utils::prelude::default;
/// The handle of the area LUT, a KTX2 format texture that SMAA uses internally.
const SMAA_AREA_LUT_TEXTURE_HANDLE: Handle<Image> =
weak_handle!("569c4d67-c7fa-4958-b1af-0836023603c0");
uuid_handle!("569c4d67-c7fa-4958-b1af-0836023603c0");
/// The handle of the search LUT, a KTX2 format texture that SMAA uses internally.
const SMAA_SEARCH_LUT_TEXTURE_HANDLE: Handle<Image> =
weak_handle!("43b97515-252e-4c8a-b9af-f2fc528a1c27");
uuid_handle!("43b97515-252e-4c8a-b9af-f2fc528a1c27");
/// Adds support for subpixel morphological antialiasing, or SMAA.
pub struct SmaaPlugin;
@ -482,21 +482,19 @@ impl SpecializedRenderPipeline for SmaaEdgeDetectionPipeline {
vertex: VertexState {
shader: self.shader.clone(),
shader_defs: shader_defs.clone(),
entry_point: "edge_detection_vertex_main".into(),
entry_point: Some("edge_detection_vertex_main".into()),
buffers: vec![],
},
fragment: Some(FragmentState {
shader: self.shader.clone(),
shader_defs,
entry_point: "luma_edge_detection_fragment_main".into(),
entry_point: Some("luma_edge_detection_fragment_main".into()),
targets: vec![Some(ColorTargetState {
format: TextureFormat::Rg8Unorm,
blend: None,
write_mask: ColorWrites::ALL,
})],
}),
push_constant_ranges: vec![],
primitive: PrimitiveState::default(),
depth_stencil: Some(DepthStencilState {
format: TextureFormat::Stencil8,
depth_write_enabled: false,
@ -509,8 +507,7 @@ impl SpecializedRenderPipeline for SmaaEdgeDetectionPipeline {
},
bias: default(),
}),
multisample: MultisampleState::default(),
zero_initialize_workgroup_memory: false,
..default()
}
}
}
@ -542,21 +539,19 @@ impl SpecializedRenderPipeline for SmaaBlendingWeightCalculationPipeline {
vertex: VertexState {
shader: self.shader.clone(),
shader_defs: shader_defs.clone(),
entry_point: "blending_weight_calculation_vertex_main".into(),
entry_point: Some("blending_weight_calculation_vertex_main".into()),
buffers: vec![],
},
fragment: Some(FragmentState {
shader: self.shader.clone(),
shader_defs,
entry_point: "blending_weight_calculation_fragment_main".into(),
entry_point: Some("blending_weight_calculation_fragment_main".into()),
targets: vec![Some(ColorTargetState {
format: TextureFormat::Rgba8Unorm,
blend: None,
write_mask: ColorWrites::ALL,
})],
}),
push_constant_ranges: vec![],
primitive: PrimitiveState::default(),
depth_stencil: Some(DepthStencilState {
format: TextureFormat::Stencil8,
depth_write_enabled: false,
@ -569,8 +564,7 @@ impl SpecializedRenderPipeline for SmaaBlendingWeightCalculationPipeline {
},
bias: default(),
}),
multisample: MultisampleState::default(),
zero_initialize_workgroup_memory: false,
..default()
}
}
}
@ -590,24 +584,20 @@ impl SpecializedRenderPipeline for SmaaNeighborhoodBlendingPipeline {
vertex: VertexState {
shader: self.shader.clone(),
shader_defs: shader_defs.clone(),
entry_point: "neighborhood_blending_vertex_main".into(),
entry_point: Some("neighborhood_blending_vertex_main".into()),
buffers: vec![],
},
fragment: Some(FragmentState {
shader: self.shader.clone(),
shader_defs,
entry_point: "neighborhood_blending_fragment_main".into(),
entry_point: Some("neighborhood_blending_fragment_main".into()),
targets: vec![Some(ColorTargetState {
format: key.texture_format,
blend: None,
write_mask: ColorWrites::ALL,
})],
}),
push_constant_ranges: vec![],
primitive: PrimitiveState::default(),
depth_stencil: None,
multisample: MultisampleState::default(),
zero_initialize_workgroup_memory: false,
..default()
}
}
}

View File

@ -21,15 +21,15 @@ use bevy_reflect::{std_traits::ReflectDefault, Reflect};
use bevy_render::{
camera::{ExtractedCamera, MipBias, TemporalJitter},
prelude::{Camera, Projection},
render_graph::{NodeRunError, RenderGraphApp, RenderGraphContext, ViewNode, ViewNodeRunner},
render_graph::{NodeRunError, RenderGraphContext, RenderGraphExt, ViewNode, ViewNodeRunner},
render_resource::{
binding_types::{sampler, texture_2d, texture_depth_2d},
BindGroupEntries, BindGroupLayout, BindGroupLayoutEntries, CachedRenderPipelineId,
ColorTargetState, ColorWrites, FilterMode, FragmentState, MultisampleState, Operations,
PipelineCache, PrimitiveState, RenderPassColorAttachment, RenderPassDescriptor,
RenderPipelineDescriptor, Sampler, SamplerBindingType, SamplerDescriptor, Shader,
ShaderStages, SpecializedRenderPipeline, SpecializedRenderPipelines, TextureDescriptor,
TextureDimension, TextureFormat, TextureSampleType, TextureUsages,
ColorTargetState, ColorWrites, FilterMode, FragmentState, Operations, PipelineCache,
RenderPassColorAttachment, RenderPassDescriptor, RenderPipelineDescriptor, Sampler,
SamplerBindingType, SamplerDescriptor, Shader, ShaderStages, SpecializedRenderPipeline,
SpecializedRenderPipelines, TextureDescriptor, TextureDimension, TextureFormat,
TextureSampleType, TextureUsages,
},
renderer::{RenderContext, RenderDevice},
sync_component::SyncComponentPlugin,
@ -38,6 +38,7 @@ use bevy_render::{
view::{ExtractedView, Msaa, ViewTarget},
ExtractSchedule, MainWorld, Render, RenderApp, RenderSystems,
};
use bevy_utils::default;
use tracing::warn;
/// Plugin for temporal anti-aliasing.
@ -320,7 +321,6 @@ impl SpecializedRenderPipeline for TaaPipeline {
fragment: Some(FragmentState {
shader: self.fragment_shader.clone(),
shader_defs,
entry_point: "taa".into(),
targets: vec![
Some(ColorTargetState {
format,
@ -333,12 +333,9 @@ impl SpecializedRenderPipeline for TaaPipeline {
write_mask: ColorWrites::ALL,
}),
],
..default()
}),
primitive: PrimitiveState::default(),
depth_stencil: None,
multisample: MultisampleState::default(),
push_constant_ranges: Vec::new(),
zero_initialize_workgroup_memory: false,
..default()
}
}
}

View File

@ -7,10 +7,12 @@ use bevy_reflect::{std_traits::ReflectDefault, Reflect, TypePath};
use core::{
any::TypeId,
hash::{Hash, Hasher},
marker::PhantomData,
};
use crossbeam_channel::{Receiver, Sender};
use disqualified::ShortName;
use thiserror::Error;
use uuid::Uuid;
/// Provides [`Handle`] and [`UntypedHandle`] _for a specific asset type_.
/// This should _only_ be used for one specific asset type.
@ -117,7 +119,7 @@ impl core::fmt::Debug for StrongHandle {
/// avoiding the need to store multiple copies of the same data.
///
/// If a [`Handle`] is [`Handle::Strong`], the [`Asset`] will be kept
/// alive until the [`Handle`] is dropped. If a [`Handle`] is [`Handle::Weak`], it does not necessarily reference a live [`Asset`],
/// alive until the [`Handle`] is dropped. If a [`Handle`] is [`Handle::Uuid`], it does not necessarily reference a live [`Asset`],
/// nor will it keep assets alive.
///
/// Modifying a *handle* will change which existing asset is referenced, but modifying the *asset*
@ -133,16 +135,16 @@ pub enum Handle<A: Asset> {
/// A "strong" reference to a live (or loading) [`Asset`]. If a [`Handle`] is [`Handle::Strong`], the [`Asset`] will be kept
/// alive until the [`Handle`] is dropped. Strong handles also provide access to additional asset metadata.
Strong(Arc<StrongHandle>),
/// A "weak" reference to an [`Asset`]. If a [`Handle`] is [`Handle::Weak`], it does not necessarily reference a live [`Asset`],
/// nor will it keep assets alive.
Weak(AssetId<A>),
/// A reference to an [`Asset`] using a stable-across-runs / const identifier. Dropping this
/// handle will not result in the asset being dropped.
Uuid(Uuid, #[reflect(ignore, clone)] PhantomData<fn() -> A>),
}
impl<T: Asset> Clone for Handle<T> {
fn clone(&self) -> Self {
match self {
Handle::Strong(handle) => Handle::Strong(handle.clone()),
Handle::Weak(id) => Handle::Weak(*id),
Handle::Uuid(uuid, ..) => Handle::Uuid(*uuid, PhantomData),
}
}
}
@ -153,7 +155,7 @@ impl<A: Asset> Handle<A> {
pub fn id(&self) -> AssetId<A> {
match self {
Handle::Strong(handle) => handle.id.typed_unchecked(),
Handle::Weak(id) => *id,
Handle::Uuid(uuid, ..) => AssetId::Uuid { uuid: *uuid },
}
}
@ -162,14 +164,14 @@ impl<A: Asset> Handle<A> {
pub fn path(&self) -> Option<&AssetPath<'static>> {
match self {
Handle::Strong(handle) => handle.path.as_ref(),
Handle::Weak(_) => None,
Handle::Uuid(..) => None,
}
}
/// Returns `true` if this is a weak handle.
/// Returns `true` if this is a uuid handle.
#[inline]
pub fn is_weak(&self) -> bool {
matches!(self, Handle::Weak(_))
pub fn is_uuid(&self) -> bool {
matches!(self, Handle::Uuid(..))
}
/// Returns `true` if this is a strong handle.
@ -178,18 +180,9 @@ impl<A: Asset> Handle<A> {
matches!(self, Handle::Strong(_))
}
/// Creates a [`Handle::Weak`] clone of this [`Handle`], which will not keep the referenced [`Asset`] alive.
#[inline]
pub fn clone_weak(&self) -> Self {
match self {
Handle::Strong(handle) => Handle::Weak(handle.id.typed_unchecked::<A>()),
Handle::Weak(id) => Handle::Weak(*id),
}
}
/// Converts this [`Handle`] to an "untyped" / "generic-less" [`UntypedHandle`], which stores the [`Asset`] type information
/// _inside_ [`UntypedHandle`]. This will return [`UntypedHandle::Strong`] for [`Handle::Strong`] and [`UntypedHandle::Weak`] for
/// [`Handle::Weak`].
/// _inside_ [`UntypedHandle`]. This will return [`UntypedHandle::Strong`] for [`Handle::Strong`] and [`UntypedHandle::Uuid`] for
/// [`Handle::Uuid`].
#[inline]
pub fn untyped(self) -> UntypedHandle {
self.into()
@ -198,7 +191,7 @@ impl<A: Asset> Handle<A> {
impl<A: Asset> Default for Handle<A> {
fn default() -> Self {
Handle::Weak(AssetId::default())
Handle::Uuid(AssetId::<A>::DEFAULT_UUID, PhantomData)
}
}
@ -214,7 +207,7 @@ impl<A: Asset> core::fmt::Debug for Handle<A> {
handle.path
)
}
Handle::Weak(id) => write!(f, "WeakHandle<{name}>({:?})", id.internal()),
Handle::Uuid(uuid, ..) => write!(f, "UuidHandle<{name}>({uuid:?})"),
}
}
}
@ -284,8 +277,13 @@ impl<A: Asset> From<&mut Handle<A>> for UntypedAssetId {
pub enum UntypedHandle {
/// A strong handle, which will keep the referenced [`Asset`] alive until all strong handles are dropped.
Strong(Arc<StrongHandle>),
/// A weak handle, which does not keep the referenced [`Asset`] alive.
Weak(UntypedAssetId),
/// A UUID handle, which does not keep the referenced [`Asset`] alive.
Uuid {
/// An identifier that records the underlying asset type.
type_id: TypeId,
/// The UUID provided during asset registration.
uuid: Uuid,
},
}
impl UntypedHandle {
@ -294,7 +292,10 @@ impl UntypedHandle {
pub fn id(&self) -> UntypedAssetId {
match self {
UntypedHandle::Strong(handle) => handle.id,
UntypedHandle::Weak(id) => *id,
UntypedHandle::Uuid { type_id, uuid } => UntypedAssetId::Uuid {
uuid: *uuid,
type_id: *type_id,
},
}
}
@ -303,16 +304,7 @@ impl UntypedHandle {
pub fn path(&self) -> Option<&AssetPath<'static>> {
match self {
UntypedHandle::Strong(handle) => handle.path.as_ref(),
UntypedHandle::Weak(_) => None,
}
}
/// Creates an [`UntypedHandle::Weak`] clone of this [`UntypedHandle`], which will not keep the referenced [`Asset`] alive.
#[inline]
pub fn clone_weak(&self) -> UntypedHandle {
match self {
UntypedHandle::Strong(handle) => UntypedHandle::Weak(handle.id),
UntypedHandle::Weak(id) => UntypedHandle::Weak(*id),
UntypedHandle::Uuid { .. } => None,
}
}
@ -321,7 +313,7 @@ impl UntypedHandle {
pub fn type_id(&self) -> TypeId {
match self {
UntypedHandle::Strong(handle) => handle.id.type_id(),
UntypedHandle::Weak(id) => id.type_id(),
UntypedHandle::Uuid { type_id, .. } => *type_id,
}
}
@ -330,7 +322,7 @@ impl UntypedHandle {
pub fn typed_unchecked<A: Asset>(self) -> Handle<A> {
match self {
UntypedHandle::Strong(handle) => Handle::Strong(handle),
UntypedHandle::Weak(id) => Handle::Weak(id.typed_unchecked::<A>()),
UntypedHandle::Uuid { uuid, .. } => Handle::Uuid(uuid, PhantomData),
}
}
@ -345,10 +337,7 @@ impl UntypedHandle {
TypeId::of::<A>(),
"The target Handle<A>'s TypeId does not match the TypeId of this UntypedHandle"
);
match self {
UntypedHandle::Strong(handle) => Handle::Strong(handle),
UntypedHandle::Weak(id) => Handle::Weak(id.typed_unchecked::<A>()),
}
self.typed_unchecked()
}
/// Converts to a typed Handle. This will panic if the internal [`TypeId`] does not match the given asset type `A`
@ -376,7 +365,7 @@ impl UntypedHandle {
pub fn meta_transform(&self) -> Option<&MetaTransform> {
match self {
UntypedHandle::Strong(handle) => handle.meta_transform.as_ref(),
UntypedHandle::Weak(_) => None,
UntypedHandle::Uuid { .. } => None,
}
}
}
@ -409,12 +398,9 @@ impl core::fmt::Debug for UntypedHandle {
handle.path
)
}
UntypedHandle::Weak(id) => write!(
f,
"WeakHandle{{ type_id: {:?}, id: {:?} }}",
id.type_id(),
id.internal()
),
UntypedHandle::Uuid { type_id, uuid } => {
write!(f, "UuidHandle{{ type_id: {type_id:?}, uuid: {uuid:?} }}",)
}
}
}
}
@ -474,7 +460,10 @@ impl<A: Asset> From<Handle<A>> for UntypedHandle {
fn from(value: Handle<A>) -> Self {
match value {
Handle::Strong(handle) => UntypedHandle::Strong(handle),
Handle::Weak(id) => UntypedHandle::Weak(id.into()),
Handle::Uuid(uuid, _) => UntypedHandle::Uuid {
type_id: TypeId::of::<A>(),
uuid,
},
}
}
}
@ -490,36 +479,37 @@ impl<A: Asset> TryFrom<UntypedHandle> for Handle<A> {
return Err(UntypedAssetConversionError::TypeIdMismatch { expected, found });
}
match value {
UntypedHandle::Strong(handle) => Ok(Handle::Strong(handle)),
UntypedHandle::Weak(id) => {
let Ok(id) = id.try_into() else {
return Err(UntypedAssetConversionError::TypeIdMismatch { expected, found });
};
Ok(Handle::Weak(id))
}
}
Ok(match value {
UntypedHandle::Strong(handle) => Handle::Strong(handle),
UntypedHandle::Uuid { uuid, .. } => Handle::Uuid(uuid, PhantomData),
})
}
}
/// Creates a weak [`Handle`] from a string literal containing a UUID.
/// Creates a [`Handle`] from a string literal containing a UUID.
///
/// # Examples
///
/// ```
/// # use bevy_asset::{Handle, weak_handle};
/// # use bevy_asset::{Handle, uuid_handle};
/// # type Shader = ();
/// const SHADER: Handle<Shader> = weak_handle!("1347c9b7-c46a-48e7-b7b8-023a354b7cac");
/// const SHADER: Handle<Shader> = uuid_handle!("1347c9b7-c46a-48e7-b7b8-023a354b7cac");
/// ```
#[macro_export]
macro_rules! weak_handle {
macro_rules! uuid_handle {
($uuid:expr) => {{
$crate::Handle::Weak($crate::AssetId::Uuid {
uuid: $crate::uuid::uuid!($uuid),
})
$crate::Handle::Uuid($crate::uuid::uuid!($uuid), core::marker::PhantomData)
}};
}
#[deprecated = "Use uuid_handle! instead"]
#[macro_export]
macro_rules! weak_handle {
($uuid:expr) => {
uuid_handle!($uuid)
};
}
/// Errors preventing the conversion of to/from an [`UntypedHandle`] and a [`Handle`].
#[derive(Error, Debug, PartialEq, Clone)]
#[non_exhaustive]
@ -559,15 +549,12 @@ mod tests {
/// Typed and Untyped `Handles` should be equivalent to each other and themselves
#[test]
fn equality() {
let typed = AssetId::<TestAsset>::Uuid { uuid: UUID_1 };
let untyped = UntypedAssetId::Uuid {
let typed = Handle::<TestAsset>::Uuid(UUID_1, PhantomData);
let untyped = UntypedHandle::Uuid {
type_id: TypeId::of::<TestAsset>(),
uuid: UUID_1,
};
let typed = Handle::Weak(typed);
let untyped = UntypedHandle::Weak(untyped);
assert_eq!(
Ok(typed.clone()),
Handle::<TestAsset>::try_from(untyped.clone())
@ -585,22 +572,17 @@ mod tests {
fn ordering() {
assert!(UUID_1 < UUID_2);
let typed_1 = AssetId::<TestAsset>::Uuid { uuid: UUID_1 };
let typed_2 = AssetId::<TestAsset>::Uuid { uuid: UUID_2 };
let untyped_1 = UntypedAssetId::Uuid {
let typed_1 = Handle::<TestAsset>::Uuid(UUID_1, PhantomData);
let typed_2 = Handle::<TestAsset>::Uuid(UUID_2, PhantomData);
let untyped_1 = UntypedHandle::Uuid {
type_id: TypeId::of::<TestAsset>(),
uuid: UUID_1,
};
let untyped_2 = UntypedAssetId::Uuid {
let untyped_2 = UntypedHandle::Uuid {
type_id: TypeId::of::<TestAsset>(),
uuid: UUID_2,
};
let typed_1 = Handle::Weak(typed_1);
let typed_2 = Handle::Weak(typed_2);
let untyped_1 = UntypedHandle::Weak(untyped_1);
let untyped_2 = UntypedHandle::Weak(untyped_2);
assert!(typed_1 < typed_2);
assert!(untyped_1 < untyped_2);
@ -617,15 +599,12 @@ mod tests {
/// Typed and Untyped `Handles` should be equivalently hashable to each other and themselves
#[test]
fn hashing() {
let typed = AssetId::<TestAsset>::Uuid { uuid: UUID_1 };
let untyped = UntypedAssetId::Uuid {
let typed = Handle::<TestAsset>::Uuid(UUID_1, PhantomData);
let untyped = UntypedHandle::Uuid {
type_id: TypeId::of::<TestAsset>(),
uuid: UUID_1,
};
let typed = Handle::Weak(typed);
let untyped = UntypedHandle::Weak(untyped);
assert_eq!(
hash(&typed),
hash(&Handle::<TestAsset>::try_from(untyped.clone()).unwrap())
@ -637,15 +616,12 @@ mod tests {
/// Typed and Untyped `Handles` should be interchangeable
#[test]
fn conversion() {
let typed = AssetId::<TestAsset>::Uuid { uuid: UUID_1 };
let untyped = UntypedAssetId::Uuid {
let typed = Handle::<TestAsset>::Uuid(UUID_1, PhantomData);
let untyped = UntypedHandle::Uuid {
type_id: TypeId::of::<TestAsset>(),
uuid: UUID_1,
};
let typed = Handle::Weak(typed);
let untyped = UntypedHandle::Weak(untyped);
assert_eq!(typed, Handle::try_from(untyped.clone()).unwrap());
assert_eq!(UntypedHandle::from(typed.clone()), untyped);
}

View File

@ -47,7 +47,7 @@ fn js_value_to_err(context: &str) -> impl FnOnce(JsValue) -> std::io::Error + '_
}
};
std::io::Error::new(std::io::ErrorKind::Other, message)
std::io::Error::other(message)
}
}
@ -62,10 +62,7 @@ impl HttpWasmAssetReader {
let worker: web_sys::WorkerGlobalScope = global.unchecked_into();
worker.fetch_with_str(path.to_str().unwrap())
} else {
let error = std::io::Error::new(
std::io::ErrorKind::Other,
"Unsupported JavaScript global context",
);
let error = std::io::Error::other("Unsupported JavaScript global context");
return Err(AssetReaderError::Io(error.into()));
};
let resp_value = JsFuture::from(promise)

View File

@ -553,7 +553,9 @@ impl AssetServer {
path: impl Into<AssetPath<'a>>,
) -> Result<UntypedHandle, AssetLoadError> {
let path: AssetPath = path.into();
self.load_internal(None, path, false, None).await
self.load_internal(None, path, false, None)
.await
.map(|h| h.expect("handle must be returned, since we didn't pass in an input handle"))
}
pub(crate) fn load_unknown_type_with_meta_transform<'a>(
@ -643,21 +645,25 @@ impl AssetServer {
/// Performs an async asset load.
///
/// `input_handle` must only be [`Some`] if `should_load` was true when retrieving `input_handle`. This is an optimization to
/// avoid looking up `should_load` twice, but it means you _must_ be sure a load is necessary when calling this function with [`Some`].
/// `input_handle` must only be [`Some`] if `should_load` was true when retrieving
/// `input_handle`. This is an optimization to avoid looking up `should_load` twice, but it
/// means you _must_ be sure a load is necessary when calling this function with [`Some`].
///
/// Returns the handle of the asset if one was retrieved by this function. Otherwise, may return
/// [`None`].
async fn load_internal<'a>(
&self,
mut input_handle: Option<UntypedHandle>,
input_handle: Option<UntypedHandle>,
path: AssetPath<'a>,
force: bool,
meta_transform: Option<MetaTransform>,
) -> Result<UntypedHandle, AssetLoadError> {
let asset_type_id = input_handle.as_ref().map(UntypedHandle::type_id);
) -> Result<Option<UntypedHandle>, AssetLoadError> {
let input_handle_type_id = input_handle.as_ref().map(UntypedHandle::type_id);
let path = path.into_owned();
let path_clone = path.clone();
let (mut meta, loader, mut reader) = self
.get_meta_loader_and_reader(&path_clone, asset_type_id)
.get_meta_loader_and_reader(&path_clone, input_handle_type_id)
.await
.inspect_err(|e| {
// if there was an input handle, a "load" operation has already started, so we must produce a "failure" event, if
@ -674,76 +680,90 @@ impl AssetServer {
if let Some(meta_transform) = input_handle.as_ref().and_then(|h| h.meta_transform()) {
(*meta_transform)(&mut *meta);
}
// downgrade the input handle so we don't keep the asset alive just because we're loading it
// note we can't just pass a weak handle in, as only strong handles contain the asset meta transform
input_handle = input_handle.map(|h| h.clone_weak());
// This contains Some(UntypedHandle), if it was retrievable
// If it is None, that is because it was _not_ retrievable, due to
// 1. The handle was not already passed in for this path, meaning we can't just use that
// 2. The asset has not been loaded yet, meaning there is no existing Handle for it
// 3. The path has a label, meaning the AssetLoader's root asset type is not the path's asset type
//
// In the None case, the only course of action is to wait for the asset to load so we can allocate the
// handle for that type.
//
// TODO: Note that in the None case, multiple asset loads for the same path can happen at the same time
// (rather than "early out-ing" in the "normal" case)
// This would be resolved by a universal asset id, as we would not need to resolve the asset type
// to generate the ID. See this issue: https://github.com/bevyengine/bevy/issues/10549
let handle_result = match input_handle {
Some(handle) => {
// if a handle was passed in, the "should load" check was already done
Some((handle, true))
}
None => {
let mut infos = self.data.infos.write();
let result = infos.get_or_create_path_handle_internal(
path.clone(),
path.label().is_none().then(|| loader.asset_type_id()),
HandleLoadingMode::Request,
meta_transform,
);
unwrap_with_context(result, Either::Left(loader.asset_type_name()))
}
};
let asset_id; // The asset ID of the asset we are trying to load.
let fetched_handle; // The handle if one was looked up/created.
let should_load; // Whether we need to load the asset.
if let Some(input_handle) = input_handle {
asset_id = Some(input_handle.id());
// In this case, we intentionally drop the input handle so we can cancel loading the
// asset if the handle gets dropped (externally) before it finishes loading.
fetched_handle = None;
// The handle was passed in, so the "should_load" check was already done.
should_load = true;
} else {
// TODO: multiple asset loads for the same path can happen at the same time (rather than
// "early out-ing" in the "normal" case). This would be resolved by a universal asset
// id, as we would not need to resolve the asset type to generate the ID. See this
// issue: https://github.com/bevyengine/bevy/issues/10549
let handle = if let Some((handle, should_load)) = handle_result {
if path.label().is_none() && handle.type_id() != loader.asset_type_id() {
let mut infos = self.data.infos.write();
let result = infos.get_or_create_path_handle_internal(
path.clone(),
path.label().is_none().then(|| loader.asset_type_id()),
HandleLoadingMode::Request,
meta_transform,
);
match unwrap_with_context(result, Either::Left(loader.asset_type_name())) {
// We couldn't figure out the correct handle without its type ID (which can only
// happen if we are loading a subasset).
None => {
// We don't know the expected type since the subasset may have a different type
// than the "root" asset (which is the type the loader will load).
asset_id = None;
fetched_handle = None;
// If we couldn't find an appropriate handle, then the asset certainly needs to
// be loaded.
should_load = true;
}
Some((handle, result_should_load)) => {
asset_id = Some(handle.id());
fetched_handle = Some(handle);
should_load = result_should_load;
}
}
}
// Verify that the expected type matches the loader's type.
if let Some(asset_type_id) = asset_id.map(|id| id.type_id()) {
// If we are loading a subasset, then the subasset's type almost certainly doesn't match
// the loader's type - and that's ok.
if path.label().is_none() && asset_type_id != loader.asset_type_id() {
error!(
"Expected {:?}, got {:?}",
handle.type_id(),
asset_type_id,
loader.asset_type_id()
);
return Err(AssetLoadError::RequestedHandleTypeMismatch {
path: path.into_owned(),
requested: handle.type_id(),
requested: asset_type_id,
actual_asset_name: loader.asset_type_name(),
loader_name: loader.type_name(),
});
}
if !should_load && !force {
return Ok(handle);
}
Some(handle)
} else {
None
};
// if the handle result is None, we definitely need to load the asset
}
// Bail out earlier if we don't need to load the asset.
if !should_load && !force {
return Ok(fetched_handle);
}
let (base_handle, base_path) = if path.label().is_some() {
// We don't actually need to use _base_handle, but we do need to keep the handle alive.
// Dropping it would cancel the load of the base asset, which would make the load of this
// subasset never complete.
let (base_asset_id, _base_handle, base_path) = if path.label().is_some() {
let mut infos = self.data.infos.write();
let base_path = path.without_label().into_owned();
let (base_handle, _) = infos.get_or_create_path_handle_erased(
base_path.clone(),
loader.asset_type_id(),
Some(loader.asset_type_name()),
HandleLoadingMode::Force,
None,
);
(base_handle, base_path)
let base_handle = infos
.get_or_create_path_handle_erased(
base_path.clone(),
loader.asset_type_id(),
Some(loader.asset_type_name()),
HandleLoadingMode::Force,
None,
)
.0;
(base_handle.id(), Some(base_handle), base_path)
} else {
(handle.clone().unwrap(), path.clone())
(asset_id.unwrap(), None, path.clone())
};
match self
@ -760,7 +780,7 @@ impl AssetServer {
Ok(loaded_asset) => {
let final_handle = if let Some(label) = path.label_cow() {
match loaded_asset.labeled_assets.get(&label) {
Some(labeled_asset) => labeled_asset.handle.clone(),
Some(labeled_asset) => Some(labeled_asset.handle.clone()),
None => {
let mut all_labels: Vec<String> = loaded_asset
.labeled_assets
@ -776,16 +796,15 @@ impl AssetServer {
}
}
} else {
// if the path does not have a label, the handle must exist at this point
handle.unwrap()
fetched_handle
};
self.send_loaded_asset(base_handle.id(), loaded_asset);
self.send_loaded_asset(base_asset_id, loaded_asset);
Ok(final_handle)
}
Err(err) => {
self.send_asset_event(InternalAssetEvent::Failed {
id: base_handle.id(),
id: base_asset_id,
error: err.clone(),
path: path.into_owned(),
});

View File

@ -4,7 +4,7 @@ use bevy_ecs::prelude::*;
use bevy_render::{
extract_component::ExtractComponentPlugin,
render_asset::RenderAssetPlugin,
render_graph::RenderGraphApp,
render_graph::RenderGraphExt,
render_resource::{
Buffer, BufferDescriptor, BufferUsages, PipelineCache, SpecializedComputePipelines,
},

View File

@ -10,6 +10,7 @@ use bevy_render::{
renderer::RenderDevice,
view::ViewUniform,
};
use bevy_utils::default;
use core::num::NonZero;
#[derive(Resource)]
@ -82,12 +83,11 @@ impl SpecializedComputePipeline for AutoExposurePipeline {
layout: vec![self.histogram_layout.clone()],
shader: self.histogram_shader.clone(),
shader_defs: vec![],
entry_point: match pass {
entry_point: Some(match pass {
AutoExposurePass::Histogram => "compute_histogram".into(),
AutoExposurePass::Average => "compute_average".into(),
},
push_constant_ranges: vec![],
zero_initialize_workgroup_memory: false,
}),
..default()
}
}
}

View File

@ -1,3 +1,4 @@
use crate::FullscreenShader;
use bevy_app::{App, Plugin};
use bevy_asset::{embedded_asset, load_embedded_asset, Handle};
use bevy_ecs::prelude::*;
@ -9,8 +10,7 @@ use bevy_render::{
renderer::RenderDevice,
RenderApp,
};
use crate::FullscreenShader;
use bevy_utils::default;
/// Adds support for specialized "blit pipelines", which can be used to write one texture to another.
pub struct BlitPlugin;
@ -85,22 +85,18 @@ impl SpecializedRenderPipeline for BlitPipeline {
vertex: self.fullscreen_shader.to_vertex_state(),
fragment: Some(FragmentState {
shader: self.fragment_shader.clone(),
shader_defs: vec![],
entry_point: "fs_main".into(),
targets: vec![Some(ColorTargetState {
format: key.texture_format,
blend: key.blend_state,
write_mask: ColorWrites::ALL,
})],
..default()
}),
primitive: PrimitiveState::default(),
depth_stencil: None,
multisample: MultisampleState {
count: key.samples,
..Default::default()
..default()
},
push_constant_ranges: Vec::new(),
zero_initialize_workgroup_memory: false,
..default()
}
}
}

View File

@ -16,6 +16,7 @@ use bevy_render::{
},
renderer::RenderDevice,
};
use bevy_utils::default;
#[derive(Component)]
pub struct BloomDownsamplingPipelineIds {
@ -130,18 +131,14 @@ impl SpecializedRenderPipeline for BloomDownsamplingPipeline {
fragment: Some(FragmentState {
shader: self.fragment_shader.clone(),
shader_defs,
entry_point,
entry_point: Some(entry_point),
targets: vec![Some(ColorTargetState {
format: BLOOM_TEXTURE_FORMAT,
blend: None,
write_mask: ColorWrites::ALL,
})],
}),
primitive: PrimitiveState::default(),
depth_stencil: None,
multisample: MultisampleState::default(),
push_constant_ranges: Vec::new(),
zero_initialize_workgroup_memory: false,
..default()
}
}
}

View File

@ -20,7 +20,7 @@ use bevy_render::{
extract_component::{
ComponentUniforms, DynamicUniformIndex, ExtractComponentPlugin, UniformComponentPlugin,
},
render_graph::{NodeRunError, RenderGraphApp, RenderGraphContext, ViewNode, ViewNodeRunner},
render_graph::{NodeRunError, RenderGraphContext, RenderGraphExt, ViewNode, ViewNodeRunner},
render_resource::*,
renderer::{RenderContext, RenderDevice},
texture::{CachedTexture, TextureCache},

View File

@ -18,6 +18,7 @@ use bevy_render::{
renderer::RenderDevice,
view::ViewTarget,
};
use bevy_utils::default;
#[derive(Component)]
pub struct UpsamplingPipelineIds {
@ -115,8 +116,7 @@ impl SpecializedRenderPipeline for BloomUpsamplingPipeline {
vertex: self.fullscreen_shader.to_vertex_state(),
fragment: Some(FragmentState {
shader: self.fragment_shader.clone(),
shader_defs: vec![],
entry_point: "upsample".into(),
entry_point: Some("upsample".into()),
targets: vec![Some(ColorTargetState {
format: texture_format,
blend: Some(BlendState {
@ -129,12 +129,9 @@ impl SpecializedRenderPipeline for BloomUpsamplingPipeline {
}),
write_mask: ColorWrites::ALL,
})],
..default()
}),
primitive: PrimitiveState::default(),
depth_stencil: None,
multisample: MultisampleState::default(),
push_constant_ranges: Vec::new(),
zero_initialize_workgroup_memory: false,
..default()
}
}
}

View File

@ -52,7 +52,7 @@ use bevy_math::FloatOrd;
use bevy_render::{
camera::{Camera, ExtractedCamera},
extract_component::ExtractComponentPlugin,
render_graph::{EmptyNode, RenderGraphApp, ViewNodeRunner},
render_graph::{EmptyNode, RenderGraphExt, ViewNodeRunner},
render_phase::{
sort_phase_system, BinnedPhaseItem, CachedRenderPipelinePhaseItem, DrawFunctionId,
DrawFunctions, PhaseItem, PhaseItemExtraIndex, SortedPhaseItem, ViewBinnedRenderPhases,

View File

@ -4,7 +4,7 @@ use crate::{
};
use bevy_ecs::{prelude::World, query::QueryItem};
use bevy_render::{
camera::ExtractedCamera,
camera::{ExtractedCamera, MainPassResolutionOverride},
diagnostic::RecordDiagnostics,
render_graph::{NodeRunError, RenderGraphContext, ViewNode},
render_phase::{TrackedRenderPass, ViewBinnedRenderPhases},
@ -31,6 +31,7 @@ impl ViewNode for MainOpaquePass3dNode {
Option<&'static SkyboxPipelineId>,
Option<&'static SkyboxBindGroup>,
&'static ViewUniformOffset,
Option<&'static MainPassResolutionOverride>,
);
fn run<'w>(
@ -45,6 +46,7 @@ impl ViewNode for MainOpaquePass3dNode {
skybox_pipeline,
skybox_bind_group,
view_uniform_offset,
resolution_override,
): QueryItem<'w, '_, Self::ViewQuery>,
world: &'w World,
) -> Result<(), NodeRunError> {
@ -90,7 +92,7 @@ impl ViewNode for MainOpaquePass3dNode {
let pass_span = diagnostics.pass_span(&mut render_pass, "main_opaque_pass_3d");
if let Some(viewport) = camera.viewport.as_ref() {
render_pass.set_camera_viewport(viewport);
render_pass.set_camera_viewport(&viewport.with_override(resolution_override));
}
// Opaque draws

View File

@ -3,7 +3,7 @@ use crate::core_3d::Transmissive3d;
use bevy_ecs::{prelude::*, query::QueryItem};
use bevy_image::ToExtents;
use bevy_render::{
camera::ExtractedCamera,
camera::{ExtractedCamera, MainPassResolutionOverride},
render_graph::{NodeRunError, RenderGraphContext, ViewNode},
render_phase::ViewSortedRenderPhases,
render_resource::{RenderPassDescriptor, StoreOp},
@ -28,13 +28,16 @@ impl ViewNode for MainTransmissivePass3dNode {
&'static ViewTarget,
Option<&'static ViewTransmissionTexture>,
&'static ViewDepthTexture,
Option<&'static MainPassResolutionOverride>,
);
fn run(
&self,
graph: &mut RenderGraphContext,
render_context: &mut RenderContext,
(camera, view, camera_3d, target, transmission, depth): QueryItem<Self::ViewQuery>,
(camera, view, camera_3d, target, transmission, depth, resolution_override): QueryItem<
Self::ViewQuery,
>,
world: &World,
) -> Result<(), NodeRunError> {
let view_entity = graph.view_entity();
@ -108,7 +111,7 @@ impl ViewNode for MainTransmissivePass3dNode {
render_context.begin_tracked_render_pass(render_pass_descriptor);
if let Some(viewport) = camera.viewport.as_ref() {
render_pass.set_camera_viewport(viewport);
render_pass.set_camera_viewport(&viewport.with_override(resolution_override));
}
if let Err(err) = transmissive_phase.render(&mut render_pass, world, view_entity) {

View File

@ -1,7 +1,7 @@
use crate::core_3d::Transparent3d;
use bevy_ecs::{prelude::*, query::QueryItem};
use bevy_render::{
camera::ExtractedCamera,
camera::{ExtractedCamera, MainPassResolutionOverride},
diagnostic::RecordDiagnostics,
render_graph::{NodeRunError, RenderGraphContext, ViewNode},
render_phase::ViewSortedRenderPhases,
@ -24,12 +24,13 @@ impl ViewNode for MainTransparentPass3dNode {
&'static ExtractedView,
&'static ViewTarget,
&'static ViewDepthTexture,
Option<&'static MainPassResolutionOverride>,
);
fn run(
&self,
graph: &mut RenderGraphContext,
render_context: &mut RenderContext,
(camera, view, target, depth): QueryItem<Self::ViewQuery>,
(camera, view, target, depth, resolution_override): QueryItem<Self::ViewQuery>,
world: &World,
) -> Result<(), NodeRunError> {
let view_entity = graph.view_entity();
@ -69,7 +70,7 @@ impl ViewNode for MainTransparentPass3dNode {
let pass_span = diagnostics.pass_span(&mut render_pass, "main_transparent_pass_3d");
if let Some(viewport) = camera.viewport.as_ref() {
render_pass.set_camera_viewport(viewport);
render_pass.set_camera_viewport(&viewport.with_override(resolution_override));
}
if let Err(err) = transparent_phase.render(&mut render_pass, world, view_entity) {

View File

@ -92,7 +92,7 @@ use bevy_render::{
camera::{Camera, ExtractedCamera},
extract_component::ExtractComponentPlugin,
prelude::Msaa,
render_graph::{EmptyNode, RenderGraphApp, ViewNodeRunner},
render_graph::{EmptyNode, RenderGraphExt, ViewNodeRunner},
render_phase::{
sort_phase_system, BinnedPhaseItem, CachedRenderPipelinePhaseItem, DrawFunctionId,
DrawFunctions, PhaseItem, PhaseItemExtraIndex, SortedPhaseItem, ViewBinnedRenderPhases,
@ -1026,7 +1026,8 @@ pub fn prepare_prepass_textures(
format: CORE_3D_DEPTH_FORMAT,
usage: TextureUsages::COPY_DST
| TextureUsages::RENDER_ATTACHMENT
| TextureUsages::TEXTURE_BINDING,
| TextureUsages::TEXTURE_BINDING
| TextureUsages::COPY_SRC, // TODO: Remove COPY_SRC, double buffer instead (for bevy_solari)
view_formats: &[],
};
texture_cache.get(&render_device, descriptor)
@ -1092,7 +1093,8 @@ pub fn prepare_prepass_textures(
dimension: TextureDimension::D2,
format: DEFERRED_PREPASS_FORMAT,
usage: TextureUsages::RENDER_ATTACHMENT
| TextureUsages::TEXTURE_BINDING,
| TextureUsages::TEXTURE_BINDING
| TextureUsages::COPY_SRC, // TODO: Remove COPY_SRC, double buffer instead (for bevy_solari)
view_formats: &[],
},
)

View File

@ -15,13 +15,13 @@ use bevy_render::{
Render, RenderApp, RenderSystems,
};
use super::DEFERRED_LIGHTING_PASS_ID_DEPTH_FORMAT;
use bevy_ecs::query::QueryItem;
use bevy_render::{
render_graph::{NodeRunError, RenderGraphContext, ViewNode},
renderer::RenderContext,
};
use super::DEFERRED_LIGHTING_PASS_ID_DEPTH_FORMAT;
use bevy_utils::default;
pub struct CopyDeferredLightingIdPlugin;
@ -142,11 +142,8 @@ impl FromWorld for CopyDeferredLightingIdPipeline {
vertex: vertex_state,
fragment: Some(FragmentState {
shader,
shader_defs: vec![],
entry_point: "fragment".into(),
targets: vec![],
..default()
}),
primitive: PrimitiveState::default(),
depth_stencil: Some(DepthStencilState {
format: DEFERRED_LIGHTING_PASS_ID_DEPTH_FORMAT,
depth_write_enabled: true,
@ -154,9 +151,7 @@ impl FromWorld for CopyDeferredLightingIdPipeline {
stencil: StencilState::default(),
bias: DepthBiasState::default(),
}),
multisample: MultisampleState::default(),
push_constant_ranges: vec![],
zero_initialize_workgroup_memory: false,
..default()
});
Self {

View File

@ -1,4 +1,5 @@
use bevy_ecs::{prelude::*, query::QueryItem};
use bevy_render::camera::MainPassResolutionOverride;
use bevy_render::experimental::occlusion_culling::OcclusionCulling;
use bevy_render::render_graph::ViewNode;
@ -66,6 +67,7 @@ impl ViewNode for LateDeferredGBufferPrepassNode {
&'static ExtractedView,
&'static ViewDepthTexture,
&'static ViewPrepassTextures,
Option<&'static MainPassResolutionOverride>,
Has<OcclusionCulling>,
Has<NoIndirectDrawing>,
);
@ -77,7 +79,7 @@ impl ViewNode for LateDeferredGBufferPrepassNode {
view_query: QueryItem<'w, '_, Self::ViewQuery>,
world: &'w World,
) -> Result<(), NodeRunError> {
let (_, _, _, _, occlusion_culling, no_indirect_drawing) = view_query;
let (.., occlusion_culling, no_indirect_drawing) = view_query;
if !occlusion_culling || no_indirect_drawing {
return Ok(());
}
@ -105,7 +107,7 @@ impl ViewNode for LateDeferredGBufferPrepassNode {
fn run_deferred_prepass<'w>(
graph: &mut RenderGraphContext,
render_context: &mut RenderContext<'w>,
(camera, extracted_view, view_depth_texture, view_prepass_textures, _, _): QueryItem<
(camera, extracted_view, view_depth_texture, view_prepass_textures, resolution_override, _, _): QueryItem<
'w,
'_,
<LateDeferredGBufferPrepassNode as ViewNode>::ViewQuery,
@ -220,7 +222,7 @@ fn run_deferred_prepass<'w>(
});
let mut render_pass = TrackedRenderPass::new(&render_device, render_pass);
if let Some(viewport) = camera.viewport.as_ref() {
render_pass.set_camera_viewport(viewport);
render_pass.set_camera_viewport(&viewport.with_override(resolution_override));
}
// Opaque draws

View File

@ -34,7 +34,7 @@ use bevy_render::{
camera::{PhysicalCameraParameters, Projection},
extract_component::{ComponentUniforms, DynamicUniformIndex, UniformComponentPlugin},
render_graph::{
NodeRunError, RenderGraphApp as _, RenderGraphContext, ViewNode, ViewNodeRunner,
NodeRunError, RenderGraphContext, RenderGraphExt as _, ViewNode, ViewNodeRunner,
},
render_resource::{
binding_types::{
@ -800,23 +800,19 @@ impl SpecializedRenderPipeline for DepthOfFieldPipeline {
RenderPipelineDescriptor {
label: Some("depth of field pipeline".into()),
layout,
push_constant_ranges: vec![],
vertex: self.fullscreen_shader.to_vertex_state(),
primitive: default(),
depth_stencil: None,
multisample: default(),
fragment: Some(FragmentState {
shader: self.fragment_shader.clone(),
shader_defs,
entry_point: match key.pass {
entry_point: Some(match key.pass {
DofPass::GaussianHorizontal => "gaussian_horizontal".into(),
DofPass::GaussianVertical => "gaussian_vertical".into(),
DofPass::BokehPass0 => "bokeh_pass_0".into(),
DofPass::BokehPass1 => "bokeh_pass_1".into(),
},
}),
targets,
}),
zero_initialize_workgroup_memory: false,
..default()
}
}
}

View File

@ -12,7 +12,7 @@ use crate::core_3d::{
prepare_core_3d_depth_textures,
};
use bevy_app::{App, Plugin};
use bevy_asset::{load_internal_asset, weak_handle, Handle};
use bevy_asset::{load_internal_asset, uuid_handle, Handle};
use bevy_derive::{Deref, DerefMut};
use bevy_ecs::{
component::Component,
@ -30,7 +30,7 @@ use bevy_render::{
experimental::occlusion_culling::{
OcclusionCulling, OcclusionCullingSubview, OcclusionCullingSubviewEntities,
},
render_graph::{Node, NodeRunError, RenderGraphApp, RenderGraphContext},
render_graph::{Node, NodeRunError, RenderGraphContext, RenderGraphExt},
render_resource::{
binding_types::{sampler, texture_2d, texture_2d_multisampled, texture_storage_2d},
BindGroup, BindGroupEntries, BindGroupLayout, BindGroupLayoutEntries,
@ -46,12 +46,13 @@ use bevy_render::{
view::{ExtractedView, NoIndirectDrawing, ViewDepthTexture},
Render, RenderApp, RenderSystems,
};
use bevy_utils::default;
use bitflags::bitflags;
use tracing::debug;
/// Identifies the `downsample_depth.wgsl` shader.
pub const DOWNSAMPLE_DEPTH_SHADER_HANDLE: Handle<Shader> =
weak_handle!("a09a149e-5922-4fa4-9170-3c1a13065364");
uuid_handle!("a09a149e-5922-4fa4-9170-3c1a13065364");
/// The maximum number of mip levels that we can produce.
///
@ -492,12 +493,12 @@ impl SpecializedComputePipeline for DownsampleDepthPipeline {
}],
shader: DOWNSAMPLE_DEPTH_SHADER_HANDLE,
shader_defs,
entry_point: if key.contains(DownsampleDepthPipelineKey::SECOND_PHASE) {
entry_point: Some(if key.contains(DownsampleDepthPipelineKey::SECOND_PHASE) {
"downsample_depth_second".into()
} else {
"downsample_depth_first".into()
},
zero_initialize_workgroup_memory: false,
}),
..default()
}
}
}

View File

@ -33,7 +33,7 @@ impl FullscreenShader {
VertexState {
shader: self.0.clone(),
shader_defs: Vec::new(),
entry_point: "fullscreen_vertex_shader".into(),
entry_point: Some("fullscreen_vertex_shader".into()),
buffers: Vec::new(),
}
}

View File

@ -18,7 +18,7 @@ use bevy_reflect::{std_traits::ReflectDefault, Reflect};
use bevy_render::{
camera::Camera,
extract_component::{ExtractComponent, ExtractComponentPlugin, UniformComponentPlugin},
render_graph::{RenderGraphApp, ViewNodeRunner},
render_graph::{RenderGraphExt, ViewNodeRunner},
render_resource::{ShaderType, SpecializedRenderPipelines},
Render, RenderApp, RenderSystems,
};

View File

@ -1,3 +1,4 @@
use crate::FullscreenShader;
use bevy_asset::{load_embedded_asset, Handle};
use bevy_ecs::{
component::Component,
@ -16,16 +17,14 @@ use bevy_render::{
texture_depth_2d_multisampled, uniform_buffer_sized,
},
BindGroupLayout, BindGroupLayoutEntries, CachedRenderPipelineId, ColorTargetState,
ColorWrites, FragmentState, MultisampleState, PipelineCache, PrimitiveState,
RenderPipelineDescriptor, Sampler, SamplerBindingType, SamplerDescriptor, Shader,
ShaderDefVal, ShaderStages, ShaderType, SpecializedRenderPipeline,
SpecializedRenderPipelines, TextureFormat, TextureSampleType,
ColorWrites, FragmentState, PipelineCache, RenderPipelineDescriptor, Sampler,
SamplerBindingType, SamplerDescriptor, Shader, ShaderDefVal, ShaderStages, ShaderType,
SpecializedRenderPipeline, SpecializedRenderPipelines, TextureFormat, TextureSampleType,
},
renderer::RenderDevice,
view::{ExtractedView, Msaa, ViewTarget},
};
use crate::FullscreenShader;
use bevy_utils::default;
use super::MotionBlurUniform;
@ -139,7 +138,6 @@ impl SpecializedRenderPipeline for MotionBlurPipeline {
fragment: Some(FragmentState {
shader: self.fragment_shader.clone(),
shader_defs,
entry_point: "fragment".into(),
targets: vec![Some(ColorTargetState {
format: if key.hdr {
ViewTarget::TEXTURE_FORMAT_HDR
@ -149,12 +147,9 @@ impl SpecializedRenderPipeline for MotionBlurPipeline {
blend: None,
write_mask: ColorWrites::ALL,
})],
..default()
}),
primitive: PrimitiveState::default(),
depth_stencil: None,
multisample: MultisampleState::default(),
push_constant_ranges: vec![],
zero_initialize_workgroup_memory: false,
..default()
}
}
}

View File

@ -8,7 +8,7 @@ use bevy_color::LinearRgba;
use bevy_ecs::{prelude::*, query::QueryItem};
use bevy_render::{
camera::ExtractedCamera,
render_graph::{NodeRunError, RenderGraphApp, RenderGraphContext, ViewNode, ViewNodeRunner},
render_graph::{NodeRunError, RenderGraphContext, RenderGraphExt, ViewNode, ViewNodeRunner},
render_resource::*,
renderer::RenderContext,
view::{Msaa, ViewTarget},

View File

@ -10,7 +10,7 @@ use bevy_render::{
camera::{Camera, ExtractedCamera},
extract_component::{ExtractComponent, ExtractComponentPlugin},
load_shader_library,
render_graph::{RenderGraphApp, ViewNodeRunner},
render_graph::{RenderGraphExt, ViewNodeRunner},
render_resource::{BufferUsages, BufferVec, DynamicUniformBuffer, ShaderType, TextureUsages},
renderer::{RenderDevice, RenderQueue},
view::Msaa,

View File

@ -1,3 +1,4 @@
use super::OitBuffers;
use crate::{oit::OrderIndependentTransparencySettings, FullscreenShader};
use bevy_app::Plugin;
use bevy_asset::{embedded_asset, load_embedded_asset, AssetServer};
@ -12,17 +13,16 @@ use bevy_render::{
binding_types::{storage_buffer_sized, texture_depth_2d, uniform_buffer},
BindGroup, BindGroupEntries, BindGroupLayout, BindGroupLayoutEntries, BlendComponent,
BlendState, CachedRenderPipelineId, ColorTargetState, ColorWrites, DownlevelFlags,
FragmentState, MultisampleState, PipelineCache, PrimitiveState, RenderPipelineDescriptor,
ShaderDefVal, ShaderStages, TextureFormat,
FragmentState, PipelineCache, RenderPipelineDescriptor, ShaderDefVal, ShaderStages,
TextureFormat,
},
renderer::{RenderAdapter, RenderDevice},
view::{ExtractedView, ViewTarget, ViewUniform, ViewUniforms},
Render, RenderApp, RenderSystems,
};
use bevy_utils::default;
use tracing::warn;
use super::OitBuffers;
/// Contains the render node used to run the resolve pass.
pub mod node;
@ -213,7 +213,6 @@ fn specialize_oit_resolve_pipeline(
resolve_pipeline.oit_depth_bind_group_layout.clone(),
],
fragment: Some(FragmentState {
entry_point: "fragment".into(),
shader: load_embedded_asset!(asset_server, "oit_resolve.wgsl"),
shader_defs: vec![ShaderDefVal::UInt(
"LAYER_COUNT".into(),
@ -227,13 +226,10 @@ fn specialize_oit_resolve_pipeline(
}),
write_mask: ColorWrites::ALL,
})],
..default()
}),
vertex: fullscreen_shader.to_vertex_state(),
primitive: PrimitiveState::default(),
depth_stencil: None,
multisample: MultisampleState::default(),
push_constant_ranges: vec![],
zero_initialize_workgroup_memory: false,
..default()
}
}

View File

@ -1,6 +1,6 @@
use bevy_ecs::{prelude::*, query::QueryItem};
use bevy_render::{
camera::ExtractedCamera,
camera::{ExtractedCamera, MainPassResolutionOverride},
render_graph::{NodeRunError, RenderGraphContext, RenderLabel, ViewNode},
render_resource::{BindGroupEntries, PipelineCache, RenderPassDescriptor},
renderer::RenderContext,
@ -23,13 +23,14 @@ impl ViewNode for OitResolveNode {
&'static ViewUniformOffset,
&'static OitResolvePipelineId,
&'static ViewDepthTexture,
Option<&'static MainPassResolutionOverride>,
);
fn run(
&self,
_graph: &mut RenderGraphContext,
render_context: &mut RenderContext,
(camera, view_target, view_uniform, oit_resolve_pipeline_id, depth): QueryItem<
(camera, view_target, view_uniform, oit_resolve_pipeline_id, depth, resolution_override): QueryItem<
Self::ViewQuery,
>,
world: &World,
@ -63,7 +64,7 @@ impl ViewNode for OitResolveNode {
});
if let Some(viewport) = camera.viewport.as_ref() {
render_pass.set_camera_viewport(viewport);
render_pass.set_camera_viewport(&viewport.with_override(resolution_override));
}
render_pass.set_render_pipeline(pipeline);

View File

@ -3,7 +3,7 @@
//! Currently, this consists only of chromatic aberration.
use bevy_app::{App, Plugin};
use bevy_asset::{embedded_asset, load_embedded_asset, weak_handle, Assets, Handle};
use bevy_asset::{embedded_asset, load_embedded_asset, uuid_handle, Assets, Handle};
use bevy_derive::{Deref, DerefMut};
use bevy_ecs::{
component::Component,
@ -23,7 +23,7 @@ use bevy_render::{
load_shader_library,
render_asset::{RenderAssetUsages, RenderAssets},
render_graph::{
NodeRunError, RenderGraphApp as _, RenderGraphContext, ViewNode, ViewNodeRunner,
NodeRunError, RenderGraphContext, RenderGraphExt as _, ViewNode, ViewNodeRunner,
},
render_resource::{
binding_types::{sampler, texture_2d, uniform_buffer},
@ -52,7 +52,7 @@ use crate::{
/// This is just a 3x1 image consisting of one red pixel, one green pixel, and
/// one blue pixel, in that order.
const DEFAULT_CHROMATIC_ABERRATION_LUT_HANDLE: Handle<Image> =
weak_handle!("dc3e3307-40a1-49bb-be6d-e0634e8836b2");
uuid_handle!("dc3e3307-40a1-49bb-be6d-e0634e8836b2");
/// The default chromatic aberration intensity amount, in a fraction of the
/// window size.
@ -326,19 +326,14 @@ impl SpecializedRenderPipeline for PostProcessingPipeline {
vertex: self.fullscreen_shader.to_vertex_state(),
fragment: Some(FragmentState {
shader: self.fragment_shader.clone(),
shader_defs: vec![],
entry_point: "fragment_main".into(),
targets: vec![Some(ColorTargetState {
format: key.texture_format,
blend: None,
write_mask: ColorWrites::ALL,
})],
..default()
}),
primitive: default(),
depth_stencil: None,
multisample: default(),
push_constant_ranges: vec![],
zero_initialize_workgroup_memory: false,
..default()
}
}
}

View File

@ -1,6 +1,6 @@
use bevy_ecs::{prelude::*, query::QueryItem};
use bevy_render::{
camera::ExtractedCamera,
camera::{ExtractedCamera, MainPassResolutionOverride},
diagnostic::RecordDiagnostics,
experimental::occlusion_culling::OcclusionCulling,
render_graph::{NodeRunError, RenderGraphContext, ViewNode},
@ -55,18 +55,25 @@ pub struct LatePrepassNode;
impl ViewNode for LatePrepassNode {
type ViewQuery = (
&'static ExtractedCamera,
&'static ExtractedView,
&'static ViewDepthTexture,
&'static ViewPrepassTextures,
&'static ViewUniformOffset,
Option<&'static DeferredPrepass>,
Option<&'static RenderSkyboxPrepassPipeline>,
Option<&'static SkyboxPrepassBindGroup>,
Option<&'static PreviousViewUniformOffset>,
Has<OcclusionCulling>,
Has<NoIndirectDrawing>,
Has<DeferredPrepass>,
(
&'static ExtractedCamera,
&'static ExtractedView,
&'static ViewDepthTexture,
&'static ViewPrepassTextures,
&'static ViewUniformOffset,
),
(
Option<&'static DeferredPrepass>,
Option<&'static RenderSkyboxPrepassPipeline>,
Option<&'static SkyboxPrepassBindGroup>,
Option<&'static PreviousViewUniformOffset>,
Option<&'static MainPassResolutionOverride>,
),
(
Has<OcclusionCulling>,
Has<NoIndirectDrawing>,
Has<DeferredPrepass>,
),
);
fn run<'w>(
@ -78,7 +85,7 @@ impl ViewNode for LatePrepassNode {
) -> Result<(), NodeRunError> {
// We only need a late prepass if we have occlusion culling and indirect
// drawing.
let (_, _, _, _, _, _, _, _, _, occlusion_culling, no_indirect_drawing, _) = query;
let (_, _, (occlusion_culling, no_indirect_drawing, _)) = query;
if !occlusion_culling || no_indirect_drawing {
return Ok(());
}
@ -100,18 +107,15 @@ fn run_prepass<'w>(
graph: &mut RenderGraphContext,
render_context: &mut RenderContext<'w>,
(
camera,
extracted_view,
view_depth_texture,
view_prepass_textures,
view_uniform_offset,
deferred_prepass,
skybox_prepass_pipeline,
skybox_prepass_bind_group,
view_prev_uniform_offset,
_,
_,
has_deferred,
(camera, extracted_view, view_depth_texture, view_prepass_textures, view_uniform_offset),
(
deferred_prepass,
skybox_prepass_pipeline,
skybox_prepass_bind_group,
view_prev_uniform_offset,
resolution_override,
),
(_, _, has_deferred),
): QueryItem<'w, '_, <LatePrepassNode as ViewNode>::ViewQuery>,
world: &'w World,
label: &'static str,
@ -183,7 +187,7 @@ fn run_prepass<'w>(
let pass_span = diagnostics.pass_span(&mut render_pass, label);
if let Some(viewport) = camera.viewport.as_ref() {
render_pass.set_camera_viewport(viewport);
render_pass.set_camera_viewport(&viewport.with_override(resolution_override));
}
// Opaque draws

View File

@ -28,6 +28,7 @@ use bevy_render::{
Render, RenderApp, RenderSystems,
};
use bevy_transform::components::Transform;
use bevy_utils::default;
use prepass::SkyboxPrepassPipeline;
use crate::{core_3d::CORE_3D_DEPTH_FORMAT, prepass::PreviousViewUniforms};
@ -192,14 +193,10 @@ impl SpecializedRenderPipeline for SkyboxPipeline {
RenderPipelineDescriptor {
label: Some("skybox_pipeline".into()),
layout: vec![self.bind_group_layout.clone()],
push_constant_ranges: Vec::new(),
vertex: VertexState {
shader: self.shader.clone(),
shader_defs: Vec::new(),
entry_point: "skybox_vertex".into(),
buffers: Vec::new(),
..default()
},
primitive: PrimitiveState::default(),
depth_stencil: Some(DepthStencilState {
format: key.depth_format,
depth_write_enabled: false,
@ -223,8 +220,6 @@ impl SpecializedRenderPipeline for SkyboxPipeline {
},
fragment: Some(FragmentState {
shader: self.shader.clone(),
shader_defs: Vec::new(),
entry_point: "skybox_fragment".into(),
targets: vec![Some(ColorTargetState {
format: if key.hdr {
ViewTarget::TEXTURE_FORMAT_HDR
@ -235,8 +230,9 @@ impl SpecializedRenderPipeline for SkyboxPipeline {
blend: None,
write_mask: ColorWrites::ALL,
})],
..default()
}),
zero_initialize_workgroup_memory: false,
..default()
}
}
}

View File

@ -87,9 +87,7 @@ impl SpecializedRenderPipeline for SkyboxPrepassPipeline {
RenderPipelineDescriptor {
label: Some("skybox_prepass_pipeline".into()),
layout: vec![self.bind_group_layout.clone()],
push_constant_ranges: vec![],
vertex: self.fullscreen_shader.to_vertex_state(),
primitive: default(),
depth_stencil: Some(DepthStencilState {
format: CORE_3D_DEPTH_FORMAT,
depth_write_enabled: false,
@ -104,11 +102,10 @@ impl SpecializedRenderPipeline for SkyboxPrepassPipeline {
},
fragment: Some(FragmentState {
shader: self.fragment_shader.clone(),
shader_defs: vec![],
entry_point: "fragment".into(),
targets: prepass_target_descriptors(key.normal_prepass, true, false),
..default()
}),
zero_initialize_workgroup_memory: false,
..default()
}
}
}

View File

@ -5,6 +5,9 @@
struct PreviousViewUniforms {
view_from_world: mat4x4<f32>,
clip_from_world: mat4x4<f32>,
clip_from_view: mat4x4<f32>,
world_from_clip: mat4x4<f32>,
view_from_clip: mat4x4<f32>,
}
@group(0) @binding(0) var<uniform> view: View;

View File

@ -279,18 +279,14 @@ impl SpecializedRenderPipeline for TonemappingPipeline {
fragment: Some(FragmentState {
shader: self.fragment_shader.clone(),
shader_defs,
entry_point: "fragment".into(),
targets: vec![Some(ColorTargetState {
format: ViewTarget::TEXTURE_FORMAT_HDR,
blend: None,
write_mask: ColorWrites::ALL,
})],
..default()
}),
primitive: PrimitiveState::default(),
depth_stencil: None,
multisample: MultisampleState::default(),
push_constant_ranges: Vec::new(),
zero_initialize_workgroup_memory: false,
..default()
}
}
}

View File

@ -3,7 +3,7 @@ name = "bevy_core_widgets"
version = "0.17.0-dev"
edition = "2024"
description = "Unstyled common widgets for Bevy Engine"
homepage = "https://bevyengine.org"
homepage = "https://bevy.org"
repository = "https://github.com/bevyengine/bevy"
license = "MIT OR Apache-2.0"
keywords = ["bevy"]

View File

@ -0,0 +1,113 @@
use bevy_ecs::system::{Commands, SystemId, SystemInput};
use bevy_ecs::world::{DeferredWorld, World};
/// A callback defines how we want to be notified when a widget changes state. Unlike an event
/// or observer, callbacks are intended for "point-to-point" communication that cuts across the
/// hierarchy of entities. Callbacks can be created in advance of the entity they are attached
/// to, and can be passed around as parameters.
///
/// Example:
/// ```
/// use bevy_app::App;
/// use bevy_core_widgets::{Callback, Notify};
/// use bevy_ecs::system::{Commands, IntoSystem};
///
/// let mut app = App::new();
///
/// // Register a one-shot system
/// fn my_callback_system() {
/// println!("Callback executed!");
/// }
///
/// let system_id = app.world_mut().register_system(my_callback_system);
///
/// // Wrap system in a callback
/// let callback = Callback::System(system_id);
///
/// // Later, when we want to execute the callback:
/// app.world_mut().commands().notify(&callback);
/// ```
#[derive(Default, Debug)]
pub enum Callback<I: SystemInput = ()> {
/// Invoke a one-shot system
System(SystemId<I>),
/// Ignore this notification
#[default]
Ignore,
}
/// Trait used to invoke a [`Callback`], unifying the API across callers.
pub trait Notify {
/// Invoke the callback with no arguments.
fn notify(&mut self, callback: &Callback<()>);
/// Invoke the callback with one argument.
fn notify_with<I>(&mut self, callback: &Callback<I>, input: I::Inner<'static>)
where
I: SystemInput<Inner<'static>: Send> + 'static;
}
impl<'w, 's> Notify for Commands<'w, 's> {
fn notify(&mut self, callback: &Callback<()>) {
match callback {
Callback::System(system_id) => self.run_system(*system_id),
Callback::Ignore => (),
}
}
fn notify_with<I>(&mut self, callback: &Callback<I>, input: I::Inner<'static>)
where
I: SystemInput<Inner<'static>: Send> + 'static,
{
match callback {
Callback::System(system_id) => self.run_system_with(*system_id, input),
Callback::Ignore => (),
}
}
}
impl Notify for World {
fn notify(&mut self, callback: &Callback<()>) {
match callback {
Callback::System(system_id) => {
let _ = self.run_system(*system_id);
}
Callback::Ignore => (),
}
}
fn notify_with<I>(&mut self, callback: &Callback<I>, input: I::Inner<'static>)
where
I: SystemInput<Inner<'static>: Send> + 'static,
{
match callback {
Callback::System(system_id) => {
let _ = self.run_system_with(*system_id, input);
}
Callback::Ignore => (),
}
}
}
impl Notify for DeferredWorld<'_> {
fn notify(&mut self, callback: &Callback<()>) {
match callback {
Callback::System(system_id) => {
self.commands().run_system(*system_id);
}
Callback::Ignore => (),
}
}
fn notify_with<I>(&mut self, callback: &Callback<I>, input: I::Inner<'static>)
where
I: SystemInput<Inner<'static>: Send> + 'static,
{
match callback {
Callback::System(system_id) => {
self.commands().run_system_with(*system_id, input);
}
Callback::Ignore => (),
}
}
}

View File

@ -7,7 +7,7 @@ use bevy_ecs::{
entity::Entity,
observer::On,
query::With,
system::{Commands, Query, SystemId},
system::{Commands, Query},
};
use bevy_input::keyboard::{KeyCode, KeyboardInput};
use bevy_input::ButtonState;
@ -15,16 +15,17 @@ use bevy_input_focus::FocusedInput;
use bevy_picking::events::{Cancel, Click, DragEnd, Pointer, Press, Release};
use bevy_ui::{InteractionDisabled, Pressed};
use crate::{Callback, Notify};
/// Headless button widget. This widget maintains a "pressed" state, which is used to
/// indicate whether the button is currently being pressed by the user. It emits a `ButtonClicked`
/// event when the button is un-pressed.
#[derive(Component, Debug)]
#[derive(Component, Default, Debug)]
#[require(AccessibilityNode(accesskit::Node::new(Role::Button)))]
pub struct CoreButton {
/// Optional system to run when the button is clicked, or when the Enter or Space key
/// is pressed while the button is focused. If this field is `None`, the button will
/// emit a `ButtonClicked` event when clicked.
pub on_click: Option<SystemId>,
/// Callback to invoke when the button is clicked, or when the `Enter` or `Space` key
/// is pressed while the button is focused.
pub on_activate: Callback,
}
fn button_on_key_event(
@ -39,10 +40,8 @@ fn button_on_key_event(
&& event.state == ButtonState::Pressed
&& (event.key_code == KeyCode::Enter || event.key_code == KeyCode::Space)
{
if let Some(on_click) = bstate.on_click {
trigger.propagate(false);
commands.run_system(on_click);
}
trigger.propagate(false);
commands.notify(&bstate.on_activate);
}
}
}
@ -56,9 +55,7 @@ fn button_on_pointer_click(
if let Ok((bstate, pressed, disabled)) = q_state.get_mut(trigger.target()) {
trigger.propagate(false);
if pressed && !disabled {
if let Some(on_click) = bstate.on_click {
commands.run_system(on_click);
}
commands.notify(&bstate.on_activate);
}
}
}

View File

@ -7,7 +7,7 @@ use bevy_ecs::system::{In, ResMut};
use bevy_ecs::{
component::Component,
observer::On,
system::{Commands, Query, SystemId},
system::{Commands, Query},
};
use bevy_input::keyboard::{KeyCode, KeyboardInput};
use bevy_input::ButtonState;
@ -15,11 +15,13 @@ use bevy_input_focus::{FocusedInput, InputFocus, InputFocusVisible};
use bevy_picking::events::{Click, Pointer};
use bevy_ui::{Checkable, Checked, InteractionDisabled};
use crate::{Callback, Notify as _};
/// Headless widget implementation for checkboxes. The [`Checked`] component represents the current
/// state of the checkbox. The `on_change` field is an optional system id that will be run when the
/// checkbox is clicked, or when the `Enter` or `Space` key is pressed while the checkbox is
/// focused. If the `on_change` field is `None`, then instead of calling a callback, the checkbox
/// will update its own [`Checked`] state directly.
/// focused. If the `on_change` field is `Callback::Ignore`, then instead of calling a callback, the
/// checkbox will update its own [`Checked`] state directly.
///
/// # Toggle switches
///
@ -29,8 +31,10 @@ use bevy_ui::{Checkable, Checked, InteractionDisabled};
#[derive(Component, Debug, Default)]
#[require(AccessibilityNode(accesskit::Node::new(Role::CheckBox)), Checkable)]
pub struct CoreCheckbox {
/// One-shot system that is run when the checkbox state needs to be changed.
pub on_change: Option<SystemId<In<bool>>>,
/// One-shot system that is run when the checkbox state needs to be changed. If this value is
/// `Callback::Ignore`, then the checkbox will update it's own internal [`Checked`] state
/// without notification.
pub on_change: Callback<In<bool>>,
}
fn checkbox_on_key_input(
@ -157,8 +161,8 @@ fn set_checkbox_state(
checkbox: &CoreCheckbox,
new_state: bool,
) {
if let Some(on_change) = checkbox.on_change {
commands.run_system_with(on_change, new_state);
if !matches!(checkbox.on_change, Callback::Ignore) {
commands.notify_with(&checkbox.on_change, new_state);
} else if new_state {
commands.entity(entity.into()).insert(Checked);
} else {

View File

@ -9,13 +9,15 @@ use bevy_ecs::{
entity::Entity,
observer::On,
query::With,
system::{Commands, Query, SystemId},
system::{Commands, Query},
};
use bevy_input::keyboard::{KeyCode, KeyboardInput};
use bevy_input::ButtonState;
use bevy_input_focus::FocusedInput;
use bevy_picking::events::{Click, Pointer};
use bevy_ui::{Checked, InteractionDisabled};
use bevy_ui::{Checkable, Checked, InteractionDisabled};
use crate::{Callback, Notify};
/// Headless widget implementation for a "radio button group". This component is used to group
/// multiple [`CoreRadio`] components together, allowing them to behave as a single unit. It
@ -36,7 +38,7 @@ use bevy_ui::{Checked, InteractionDisabled};
#[require(AccessibilityNode(accesskit::Node::new(Role::RadioGroup)))]
pub struct CoreRadioGroup {
/// Callback which is called when the selected radio button changes.
pub on_change: Option<SystemId<In<Entity>>>,
pub on_change: Callback<In<Entity>>,
}
/// Headless widget implementation for radio buttons. These should be enclosed within a
@ -46,7 +48,7 @@ pub struct CoreRadioGroup {
/// but rather the enclosing group should be focusable.
/// See <https://www.w3.org/WAI/ARIA/apg/patterns/radio>/
#[derive(Component, Debug)]
#[require(AccessibilityNode(accesskit::Node::new(Role::RadioButton)), Checked)]
#[require(AccessibilityNode(accesskit::Node::new(Role::RadioButton)), Checkable)]
pub struct CoreRadio;
fn radio_group_on_key_input(
@ -131,9 +133,7 @@ fn radio_group_on_key_input(
let (next_id, _) = radio_buttons[next_index];
// Trigger the on_change event for the newly checked radio button
if let Some(on_change) = on_change {
commands.run_system_with(*on_change, next_id);
}
commands.notify_with(on_change, next_id);
}
}
}
@ -170,6 +170,11 @@ fn radio_group_on_button_click(
}
};
// Radio button is disabled.
if q_radio.get(radio_id).unwrap().1 {
return;
}
// Gather all the enabled radio group descendants for exclusion.
let radio_buttons = q_children
.iter_descendants(ev.target())
@ -196,9 +201,7 @@ fn radio_group_on_button_click(
}
// Trigger the on_change event for the newly checked radio button
if let Some(on_change) = on_change {
commands.run_system_with(*on_change, radio_id);
}
commands.notify_with(on_change, radio_id);
}
}

View File

@ -0,0 +1,329 @@
use bevy_app::{App, Plugin, PostUpdate};
use bevy_ecs::{
component::Component,
entity::Entity,
hierarchy::{ChildOf, Children},
observer::On,
query::{With, Without},
system::{Query, Res},
};
use bevy_math::Vec2;
use bevy_picking::events::{Cancel, Drag, DragEnd, DragStart, Pointer, Press};
use bevy_ui::{
ComputedNode, ComputedNodeTarget, Node, ScrollPosition, UiGlobalTransform, UiScale, Val,
};
/// Used to select the orientation of a scrollbar, slider, or other oriented control.
// TODO: Move this to a more central place.
#[derive(Debug, Default, Clone, Copy, PartialEq)]
pub enum ControlOrientation {
/// Horizontal orientation (stretching from left to right)
Horizontal,
/// Vertical orientation (stretching from top to bottom)
#[default]
Vertical,
}
/// A headless scrollbar widget, which can be used to build custom scrollbars.
///
/// Scrollbars operate differently than the other core widgets in a number of respects.
///
/// Unlike sliders, scrollbars don't have an [`AccessibilityNode`](bevy_a11y::AccessibilityNode)
/// component, nor can they have keyboard focus. This is because scrollbars are usually used in
/// conjunction with a scrollable container, which is itself accessible and focusable. This also
/// means that scrollbars don't accept keyboard events, which is also the responsibility of the
/// scrollable container.
///
/// Scrollbars don't emit notification events; instead they modify the scroll position of the target
/// entity directly.
///
/// A scrollbar can have any number of child entities, but one entity must be the scrollbar thumb,
/// which is marked with the [`CoreScrollbarThumb`] component. Other children are ignored. The core
/// scrollbar will directly update the position and size of this entity; the application is free to
/// set any other style properties as desired.
///
/// The application is free to position the scrollbars relative to the scrolling container however
/// it wants: it can overlay them on top of the scrolling content, or use a grid layout to displace
/// the content to make room for the scrollbars.
#[derive(Component, Debug)]
pub struct CoreScrollbar {
/// Entity being scrolled.
pub target: Entity,
/// Whether the scrollbar is vertical or horizontal.
pub orientation: ControlOrientation,
/// Minimum length of the scrollbar thumb, in pixel units, in the direction parallel to the main
/// scrollbar axis. The scrollbar will resize the thumb entity based on the proportion of
/// visible size to content size, but no smaller than this. This prevents the thumb from
/// disappearing in cases where the ratio of content size to visible size is large.
pub min_thumb_length: f32,
}
/// Marker component to indicate that the entity is a scrollbar thumb (the moving, draggable part of
/// the scrollbar). This should be a child of the scrollbar entity.
#[derive(Component, Debug)]
#[require(CoreScrollbarDragState)]
pub struct CoreScrollbarThumb;
impl CoreScrollbar {
/// Construct a new scrollbar.
///
/// # Arguments
///
/// * `target` - The scrollable entity that this scrollbar will control.
/// * `orientation` - The orientation of the scrollbar (horizontal or vertical).
/// * `min_thumb_length` - The minimum size of the scrollbar's thumb, in pixels.
pub fn new(target: Entity, orientation: ControlOrientation, min_thumb_length: f32) -> Self {
Self {
target,
orientation,
min_thumb_length,
}
}
}
/// Component used to manage the state of a scrollbar during dragging. This component is
/// inserted on the thumb entity.
#[derive(Component, Default)]
pub struct CoreScrollbarDragState {
/// Whether the scrollbar is currently being dragged.
pub dragging: bool,
/// The value of the scrollbar when dragging started.
drag_origin: f32,
}
fn scrollbar_on_pointer_down(
mut ev: On<Pointer<Press>>,
q_thumb: Query<&ChildOf, With<CoreScrollbarThumb>>,
mut q_scrollbar: Query<(
&CoreScrollbar,
&ComputedNode,
&ComputedNodeTarget,
&UiGlobalTransform,
)>,
mut q_scroll_pos: Query<(&mut ScrollPosition, &ComputedNode), Without<CoreScrollbar>>,
ui_scale: Res<UiScale>,
) {
if q_thumb.contains(ev.target()) {
// If they click on the thumb, do nothing. This will be handled by the drag event.
ev.propagate(false);
} else if let Ok((scrollbar, node, node_target, transform)) = q_scrollbar.get_mut(ev.target()) {
// If they click on the scrollbar track, page up or down.
ev.propagate(false);
// Convert to widget-local coordinates.
let local_pos = transform.try_inverse().unwrap().transform_point2(
ev.event().pointer_location.position * node_target.scale_factor() / ui_scale.0,
) + node.size() * 0.5;
// Bail if we don't find the target entity.
let Ok((mut scroll_pos, scroll_content)) = q_scroll_pos.get_mut(scrollbar.target) else {
return;
};
// Convert the click coordinates into a scroll position. If it's greater than the
// current scroll position, scroll forward by one step (visible size) otherwise scroll
// back.
let visible_size = scroll_content.size() * scroll_content.inverse_scale_factor;
let content_size = scroll_content.content_size() * scroll_content.inverse_scale_factor;
let max_range = (content_size - visible_size).max(Vec2::ZERO);
fn adjust_scroll_pos(scroll_pos: &mut f32, click_pos: f32, step: f32, range: f32) {
*scroll_pos =
(*scroll_pos + if click_pos > *scroll_pos { step } else { -step }).clamp(0., range);
}
match scrollbar.orientation {
ControlOrientation::Horizontal => {
if node.size().x > 0. {
let click_pos = local_pos.x * content_size.x / node.size().x;
adjust_scroll_pos(&mut scroll_pos.x, click_pos, visible_size.x, max_range.x);
}
}
ControlOrientation::Vertical => {
if node.size().y > 0. {
let click_pos = local_pos.y * content_size.y / node.size().y;
adjust_scroll_pos(&mut scroll_pos.y, click_pos, visible_size.y, max_range.y);
}
}
}
}
}
fn scrollbar_on_drag_start(
mut ev: On<Pointer<DragStart>>,
mut q_thumb: Query<(&ChildOf, &mut CoreScrollbarDragState), With<CoreScrollbarThumb>>,
q_scrollbar: Query<&CoreScrollbar>,
q_scroll_area: Query<&ScrollPosition>,
) {
if let Ok((ChildOf(thumb_parent), mut drag)) = q_thumb.get_mut(ev.target()) {
ev.propagate(false);
if let Ok(scrollbar) = q_scrollbar.get(*thumb_parent) {
if let Ok(scroll_area) = q_scroll_area.get(scrollbar.target) {
drag.dragging = true;
drag.drag_origin = match scrollbar.orientation {
ControlOrientation::Horizontal => scroll_area.x,
ControlOrientation::Vertical => scroll_area.y,
};
}
}
}
}
fn scrollbar_on_drag(
mut ev: On<Pointer<Drag>>,
mut q_thumb: Query<(&ChildOf, &mut CoreScrollbarDragState), With<CoreScrollbarThumb>>,
mut q_scrollbar: Query<(&ComputedNode, &CoreScrollbar)>,
mut q_scroll_pos: Query<(&mut ScrollPosition, &ComputedNode), Without<CoreScrollbar>>,
ui_scale: Res<UiScale>,
) {
if let Ok((ChildOf(thumb_parent), drag)) = q_thumb.get_mut(ev.target()) {
if let Ok((node, scrollbar)) = q_scrollbar.get_mut(*thumb_parent) {
ev.propagate(false);
let Ok((mut scroll_pos, scroll_content)) = q_scroll_pos.get_mut(scrollbar.target)
else {
return;
};
if drag.dragging {
let distance = ev.event().distance / ui_scale.0;
let visible_size = scroll_content.size() * scroll_content.inverse_scale_factor;
let content_size =
scroll_content.content_size() * scroll_content.inverse_scale_factor;
let scrollbar_size = (node.size() * node.inverse_scale_factor).max(Vec2::ONE);
match scrollbar.orientation {
ControlOrientation::Horizontal => {
let range = (content_size.x - visible_size.x).max(0.);
scroll_pos.x = (drag.drag_origin
+ (distance.x * content_size.x) / scrollbar_size.x)
.clamp(0., range);
}
ControlOrientation::Vertical => {
let range = (content_size.y - visible_size.y).max(0.);
scroll_pos.y = (drag.drag_origin
+ (distance.y * content_size.y) / scrollbar_size.y)
.clamp(0., range);
}
};
}
}
}
}
fn scrollbar_on_drag_end(
mut ev: On<Pointer<DragEnd>>,
mut q_thumb: Query<&mut CoreScrollbarDragState, With<CoreScrollbarThumb>>,
) {
if let Ok(mut drag) = q_thumb.get_mut(ev.target()) {
ev.propagate(false);
if drag.dragging {
drag.dragging = false;
}
}
}
fn scrollbar_on_drag_cancel(
mut ev: On<Pointer<Cancel>>,
mut q_thumb: Query<&mut CoreScrollbarDragState, With<CoreScrollbarThumb>>,
) {
if let Ok(mut drag) = q_thumb.get_mut(ev.target()) {
ev.propagate(false);
if drag.dragging {
drag.dragging = false;
}
}
}
fn update_scrollbar_thumb(
q_scroll_area: Query<(&ScrollPosition, &ComputedNode)>,
q_scrollbar: Query<(&CoreScrollbar, &ComputedNode, &Children)>,
mut q_thumb: Query<&mut Node, With<CoreScrollbarThumb>>,
) {
for (scrollbar, scrollbar_node, children) in q_scrollbar.iter() {
let Ok(scroll_area) = q_scroll_area.get(scrollbar.target) else {
continue;
};
// Size of the visible scrolling area.
let visible_size = scroll_area.1.size() * scroll_area.1.inverse_scale_factor;
// Size of the scrolling content.
let content_size = scroll_area.1.content_size() * scroll_area.1.inverse_scale_factor;
// Length of the scrollbar track.
let track_length = scrollbar_node.size() * scrollbar_node.inverse_scale_factor;
fn size_and_pos(
content_size: f32,
visible_size: f32,
track_length: f32,
min_size: f32,
offset: f32,
) -> (f32, f32) {
let thumb_size = if content_size > visible_size {
(track_length * visible_size / content_size)
.max(min_size)
.min(track_length)
} else {
track_length
};
let thumb_pos = if content_size > visible_size {
offset * (track_length - thumb_size) / (content_size - visible_size)
} else {
0.
};
(thumb_size, thumb_pos)
}
for child in children {
if let Ok(mut thumb) = q_thumb.get_mut(*child) {
match scrollbar.orientation {
ControlOrientation::Horizontal => {
let (thumb_size, thumb_pos) = size_and_pos(
content_size.x,
visible_size.x,
track_length.x,
scrollbar.min_thumb_length,
scroll_area.0.x,
);
thumb.top = Val::Px(0.);
thumb.bottom = Val::Px(0.);
thumb.left = Val::Px(thumb_pos);
thumb.width = Val::Px(thumb_size);
}
ControlOrientation::Vertical => {
let (thumb_size, thumb_pos) = size_and_pos(
content_size.y,
visible_size.y,
track_length.y,
scrollbar.min_thumb_length,
scroll_area.0.y,
);
thumb.left = Val::Px(0.);
thumb.right = Val::Px(0.);
thumb.top = Val::Px(thumb_pos);
thumb.height = Val::Px(thumb_size);
}
};
}
}
}
}
/// Plugin that adds the observers for the [`CoreScrollbar`] widget.
pub struct CoreScrollbarPlugin;
impl Plugin for CoreScrollbarPlugin {
fn build(&self, app: &mut App) {
app.add_observer(scrollbar_on_pointer_down)
.add_observer(scrollbar_on_drag_start)
.add_observer(scrollbar_on_drag_end)
.add_observer(scrollbar_on_drag_cancel)
.add_observer(scrollbar_on_drag)
.add_systems(PostUpdate, update_scrollbar_thumb);
}
}

View File

@ -13,7 +13,7 @@ use bevy_ecs::{
component::Component,
observer::On,
query::With,
system::{Commands, Query, SystemId},
system::{Commands, Query},
};
use bevy_input::keyboard::{KeyCode, KeyboardInput};
use bevy_input::ButtonState;
@ -22,6 +22,8 @@ use bevy_log::warn_once;
use bevy_picking::events::{Drag, DragEnd, DragStart, Pointer, Press};
use bevy_ui::{ComputedNode, ComputedNodeTarget, InteractionDisabled, UiGlobalTransform, UiScale};
use crate::{Callback, Notify};
/// Defines how the slider should behave when you click on the track (not the thumb).
#[derive(Debug, Default, PartialEq, Clone, Copy)]
pub enum TrackClick {
@ -72,8 +74,9 @@ pub enum TrackClick {
)]
pub struct CoreSlider {
/// Callback which is called when the slider is dragged or the value is changed via other user
/// interaction. If this value is `None`, then the slider will self-update.
pub on_change: Option<SystemId<In<f32>>>,
/// interaction. If this value is `Callback::Ignore`, then the slider will update it's own
/// internal [`SliderValue`] state without notification.
pub on_change: Callback<In<f32>>,
/// Set the track-clicking behavior for this slider.
pub track_click: TrackClick,
// TODO: Think about whether we want a "vertical" option.
@ -92,7 +95,9 @@ pub struct SliderValue(pub f32);
#[derive(Component, Debug, PartialEq, Clone, Copy)]
#[component(immutable)]
pub struct SliderRange {
/// The beginning of the allowed range for the slider value.
start: f32,
/// The end of the allowed range for the slider value.
end: f32,
}
@ -255,12 +260,12 @@ pub(crate) fn slider_on_pointer_down(
TrackClick::Snap => click_val,
});
if let Some(on_change) = slider.on_change {
commands.run_system_with(on_change, new_value);
} else {
if matches!(slider.on_change, Callback::Ignore) {
commands
.entity(trigger.target())
.insert(SliderValue(new_value));
} else {
commands.notify_with(&slider.on_change, new_value);
}
}
}
@ -320,12 +325,12 @@ pub(crate) fn slider_on_drag(
range.start() + span * 0.5
};
if let Some(on_change) = slider.on_change {
commands.run_system_with(on_change, new_value);
} else {
if matches!(slider.on_change, Callback::Ignore) {
commands
.entity(trigger.target())
.insert(SliderValue(new_value));
} else {
commands.notify_with(&slider.on_change, new_value);
}
}
}
@ -367,12 +372,12 @@ fn slider_on_key_input(
}
};
trigger.propagate(false);
if let Some(on_change) = slider.on_change {
commands.run_system_with(on_change, new_value);
} else {
if matches!(slider.on_change, Callback::Ignore) {
commands
.entity(trigger.target())
.insert(SliderValue(new_value));
} else {
commands.notify_with(&slider.on_change, new_value);
}
}
}
@ -459,12 +464,12 @@ fn slider_on_set_value(
range.clamp(value.0 + *delta * step.map(|s| s.0).unwrap_or_default())
}
};
if let Some(on_change) = slider.on_change {
commands.run_system_with(on_change, new_value);
} else {
if matches!(slider.on_change, Callback::Ignore) {
commands
.entity(trigger.target())
.insert(SliderValue(new_value));
} else {
commands.notify_with(&slider.on_change, new_value);
}
}
}

View File

@ -14,16 +14,23 @@
// styled/opinionated widgets that use them. Components which are directly exposed to users above
// the widget level, like `SliderValue`, should not have the `Core` prefix.
mod callback;
mod core_button;
mod core_checkbox;
mod core_radio;
mod core_scrollbar;
mod core_slider;
use bevy_app::{App, Plugin};
pub use callback::{Callback, Notify};
pub use core_button::{CoreButton, CoreButtonPlugin};
pub use core_checkbox::{CoreCheckbox, CoreCheckboxPlugin, SetChecked, ToggleChecked};
pub use core_radio::{CoreRadio, CoreRadioGroup, CoreRadioGroupPlugin};
pub use core_scrollbar::{
ControlOrientation, CoreScrollbar, CoreScrollbarDragState, CoreScrollbarPlugin,
CoreScrollbarThumb,
};
pub use core_slider::{
CoreSlider, CoreSliderDragState, CoreSliderPlugin, CoreSliderThumb, SetSliderValue,
SliderRange, SliderStep, SliderValue, TrackClick,
@ -39,6 +46,7 @@ impl Plugin for CoreWidgetsPlugin {
CoreButtonPlugin,
CoreCheckboxPlugin,
CoreRadioGroupPlugin,
CoreScrollbarPlugin,
CoreSliderPlugin,
));
}

View File

@ -40,19 +40,26 @@ pub fn derive_entity_event(input: TokenStream) -> TokenStream {
let mut traversal: Type = parse_quote!(());
let bevy_ecs_path: Path = crate::bevy_ecs_path();
let mut processed_attrs = Vec::new();
ast.generics
.make_where_clause()
.predicates
.push(parse_quote! { Self: Send + Sync + 'static });
if let Some(attr) = ast.attrs.iter().find(|attr| attr.path().is_ident(EVENT)) {
for attr in ast.attrs.iter().filter(|attr| attr.path().is_ident(EVENT)) {
if let Err(e) = attr.parse_nested_meta(|meta| match meta.path.get_ident() {
Some(ident) if processed_attrs.iter().any(|i| ident == i) => {
Err(meta.error(format!("duplicate attribute: {ident}")))
}
Some(ident) if ident == AUTO_PROPAGATE => {
auto_propagate = true;
processed_attrs.push(AUTO_PROPAGATE);
Ok(())
}
Some(ident) if ident == TRAVERSAL => {
traversal = meta.value()?.parse()?;
processed_attrs.push(TRAVERSAL);
Ok(())
}
Some(ident) => Err(meta.error(format!("unsupported attribute: {ident}"))),
@ -108,6 +115,7 @@ pub fn derive_resource(input: TokenStream) -> TokenStream {
})
}
/// Component derive syntax is documented on both the macro and the trait.
pub fn derive_component(input: TokenStream) -> TokenStream {
let mut ast = parse_macro_input!(input as DeriveInput);
let bevy_ecs_path: Path = crate::bevy_ecs_path();
@ -446,7 +454,11 @@ pub const MAP_ENTITIES: &str = "map_entities";
pub const IMMUTABLE: &str = "immutable";
pub const CLONE_BEHAVIOR: &str = "clone_behavior";
/// All allowed attribute value expression kinds for component hooks
/// All allowed attribute value expression kinds for component hooks.
/// This doesn't simply use general expressions because of conflicting needs:
/// - we want to be able to use `Self` & generic parameters in paths
/// - call expressions producing a closure need to be wrapped in a function
/// to turn them into function pointers, which prevents access to the outer generic params
#[derive(Debug)]
enum HookAttributeKind {
/// expressions like function or struct names

View File

@ -560,7 +560,17 @@ pub fn derive_event(input: TokenStream) -> TokenStream {
component::derive_event(input)
}
/// Implement the `EntityEvent` trait.
/// Cheat sheet for derive syntax,
/// see full explanation on `EntityEvent` trait docs.
///
/// ```ignore
/// #[derive(Event, EntityEvent)]
/// /// Traversal component
/// #[entity_event(traversal = &'static ChildOf)]
/// /// Always propagate
/// #[entity_event(auto_propagate)]
/// struct MyEvent;
/// ```
#[proc_macro_derive(EntityEvent, attributes(entity_event))]
pub fn derive_entity_event(input: TokenStream) -> TokenStream {
component::derive_entity_event(input)
@ -578,7 +588,89 @@ pub fn derive_resource(input: TokenStream) -> TokenStream {
component::derive_resource(input)
}
/// Implement the `Component` trait.
/// Cheat sheet for derive syntax,
/// see full explanation and examples on the `Component` trait doc.
///
/// ## Immutability
/// ```ignore
/// #[derive(Component)]
/// #[component(immutable)]
/// struct MyComponent;
/// ```
///
/// ## Sparse instead of table-based storage
/// ```ignore
/// #[derive(Component)]
/// #[component(storage = "SparseSet")]
/// struct MyComponent;
/// ```
///
/// ## Required Components
///
/// ```ignore
/// #[derive(Component)]
/// #[require(
/// // `Default::default()`
/// A,
/// // tuple structs
/// B(1),
/// // named-field structs
/// C {
/// x: 1,
/// ..default()
/// },
/// // unit structs/variants
/// D::One,
/// // associated consts
/// E::ONE,
/// // constructors
/// F::new(1),
/// // arbitrary expressions
/// G = make(1, 2, 3)
/// )]
/// struct MyComponent;
/// ```
///
/// ## Relationships
/// ```ignore
/// #[derive(Component)]
/// #[relationship(relationship_target = Children)]
/// pub struct ChildOf {
/// // Marking the field is not necessary if there is only one.
/// #[relationship]
/// pub parent: Entity,
/// internal: u8,
/// };
///
/// #[derive(Component)]
/// #[relationship_target(relationship = ChildOf)]
/// pub struct Children(Vec<Entity>);
/// ```
///
/// On despawn, also despawn all related entities:
/// ```ignore
/// #[derive(Component)]
/// #[relationship_target(relationship_target = Children, linked_spawn)]
/// pub struct Children(Vec<Entity>);
/// ```
///
/// ## Hooks
/// ```ignore
/// #[derive(Component)]
/// #[component(hook_name = function)]
/// struct MyComponent;
/// ```
/// where `hook_name` is `on_add`, `on_insert`, `on_replace` or `on_remove`;
/// `function` can be either a path, e.g. `some_function::<Self>`,
/// or a function call that returns a function that can be turned into
/// a `ComponentHook`, e.g. `get_closure("Hi!")`.
///
/// ## Ignore this component when cloning an entity
/// ```ignore
/// #[derive(Component)]
/// #[component(clone_behavior = Ignore)]
/// struct MyComponent;
/// ```
#[proc_macro_derive(
Component,
attributes(component, require, relationship, relationship_target, entities)

View File

@ -1,5 +1,5 @@
pub use bevy_ecs_macros::MapEntities;
use indexmap::IndexSet;
use indexmap::{IndexMap, IndexSet};
use crate::{
entity::{hash_map::EntityHashMap, Entity},
@ -7,11 +7,14 @@ use crate::{
};
use alloc::{
collections::{BTreeSet, VecDeque},
collections::{BTreeMap, BTreeSet, VecDeque},
vec::Vec,
};
use bevy_platform::collections::HashSet;
use core::{hash::BuildHasher, mem};
use bevy_platform::collections::{HashMap, HashSet};
use core::{
hash::{BuildHasher, Hash},
mem,
};
use smallvec::SmallVec;
use super::EntityIndexSet;
@ -72,9 +75,22 @@ impl<T: MapEntities> MapEntities for Option<T> {
}
}
impl<T: MapEntities + Eq + core::hash::Hash, S: BuildHasher + Default> MapEntities
for HashSet<T, S>
impl<K: MapEntities + Eq + Hash, V: MapEntities, S: BuildHasher + Default> MapEntities
for HashMap<K, V, S>
{
fn map_entities<E: EntityMapper>(&mut self, entity_mapper: &mut E) {
*self = self
.drain()
.map(|(mut key_entities, mut value_entities)| {
key_entities.map_entities(entity_mapper);
value_entities.map_entities(entity_mapper);
(key_entities, value_entities)
})
.collect();
}
}
impl<T: MapEntities + Eq + Hash, S: BuildHasher + Default> MapEntities for HashSet<T, S> {
fn map_entities<E: EntityMapper>(&mut self, entity_mapper: &mut E) {
*self = self
.drain()
@ -86,9 +102,22 @@ impl<T: MapEntities + Eq + core::hash::Hash, S: BuildHasher + Default> MapEntiti
}
}
impl<T: MapEntities + Eq + core::hash::Hash, S: BuildHasher + Default> MapEntities
for IndexSet<T, S>
impl<K: MapEntities + Eq + Hash, V: MapEntities, S: BuildHasher + Default> MapEntities
for IndexMap<K, V, S>
{
fn map_entities<E: EntityMapper>(&mut self, entity_mapper: &mut E) {
*self = self
.drain(..)
.map(|(mut key_entities, mut value_entities)| {
key_entities.map_entities(entity_mapper);
value_entities.map_entities(entity_mapper);
(key_entities, value_entities)
})
.collect();
}
}
impl<T: MapEntities + Eq + Hash, S: BuildHasher + Default> MapEntities for IndexSet<T, S> {
fn map_entities<E: EntityMapper>(&mut self, entity_mapper: &mut E) {
*self = self
.drain(..)
@ -109,6 +138,19 @@ impl MapEntities for EntityIndexSet {
}
}
impl<K: MapEntities + Ord, V: MapEntities> MapEntities for BTreeMap<K, V> {
fn map_entities<E: EntityMapper>(&mut self, entity_mapper: &mut E) {
*self = mem::take(self)
.into_iter()
.map(|(mut key_entities, mut value_entities)| {
key_entities.map_entities(entity_mapper);
value_entities.map_entities(entity_mapper);
(key_entities, value_entities)
})
.collect();
}
}
impl<T: MapEntities + Ord> MapEntities for BTreeSet<T> {
fn map_entities<E: EntityMapper>(&mut self, entity_mapper: &mut E) {
*self = mem::take(self)
@ -121,6 +163,14 @@ impl<T: MapEntities + Ord> MapEntities for BTreeSet<T> {
}
}
impl<T: MapEntities, const N: usize> MapEntities for [T; N] {
fn map_entities<E: EntityMapper>(&mut self, entity_mapper: &mut E) {
for entities in self.iter_mut() {
entities.map_entities(entity_mapper);
}
}
}
impl<T: MapEntities> MapEntities for Vec<T> {
fn map_entities<E: EntityMapper>(&mut self, entity_mapper: &mut E) {
for entities in self.iter_mut() {

View File

@ -20,6 +20,8 @@ pub trait HandleError<Out = ()>: Send + 'static {
/// Takes a [`Command`] that returns a Result and uses the default error handler function to convert it into
/// a [`Command`] that internally handles an error if it occurs and returns `()`.
fn handle_error(self) -> impl Command;
/// Takes a [`Command`] that returns a Result and ignores any error that occurs.
fn ignore_error(self) -> impl Command;
}
impl<C, T, E> HandleError<Result<T, E>> for C
@ -50,6 +52,12 @@ where
),
}
}
fn ignore_error(self) -> impl Command {
move |world: &mut World| {
let _ = self.apply(world);
}
}
}
impl<C> HandleError<Never> for C
@ -68,6 +76,13 @@ where
self.apply(world);
}
}
#[inline]
fn ignore_error(self) -> impl Command {
move |world: &mut World| {
self.apply(world);
}
}
}
impl<C> HandleError for C
@ -82,6 +97,10 @@ where
fn handle_error(self) -> impl Command {
self
}
#[inline]
fn ignore_error(self) -> impl Command {
self
}
}
/// Passes in a specific entity to an [`EntityCommand`], resulting in a [`Command`] that

View File

@ -14,7 +14,7 @@ pub use relationship_source_collection::*;
use crate::{
component::{Component, Mutable},
entity::{ComponentCloneCtx, Entity, SourceComponent},
error::{ignore, CommandWithEntity, HandleError},
error::CommandWithEntity,
lifecycle::HookContext,
world::{DeferredWorld, EntityWorldMut},
};
@ -126,16 +126,18 @@ pub trait Relationship: Component + Sized {
world.commands().entity(entity).remove::<Self>();
return;
}
if let Ok(mut target_entity_mut) = world.get_entity_mut(target_entity) {
if let Some(mut relationship_target) =
target_entity_mut.get_mut::<Self::RelationshipTarget>()
{
relationship_target.collection_mut_risky().add(entity);
} else {
let mut target = <Self::RelationshipTarget as RelationshipTarget>::with_capacity(1);
target.collection_mut_risky().add(entity);
world.commands().entity(target_entity).insert(target);
}
if let Ok(mut entity_commands) = world.commands().get_entity(target_entity) {
// Deferring is necessary for batch mode
entity_commands
.entry::<Self::RelationshipTarget>()
.and_modify(move |mut relationship_target| {
relationship_target.collection_mut_risky().add(entity);
})
.or_insert_with(|| {
let mut target = Self::RelationshipTarget::with_capacity(1);
target.collection_mut_risky().add(entity);
target
});
} else {
warn!(
"{}The {}({target_entity:?}) relationship on entity {entity:?} relates to an entity that does not exist. The invalid {} relationship has been removed.",
@ -187,7 +189,7 @@ pub trait Relationship: Component + Sized {
world
.commands()
.queue(command.with_entity(target_entity).handle_error_with(ignore));
.queue_silenced(command.with_entity(target_entity));
}
}
}
@ -244,7 +246,7 @@ pub trait RelationshipTarget: Component<Mutability = Mutable> + Sized {
for source_entity in relationship_target.iter() {
commands
.entity(source_entity)
.remove::<Self::Relationship>();
.try_remove::<Self::Relationship>();
}
}
@ -255,7 +257,7 @@ pub trait RelationshipTarget: Component<Mutability = Mutable> + Sized {
let (entities, mut commands) = world.entities_and_commands();
let relationship_target = entities.get(entity).unwrap().get::<Self>().unwrap();
for source_entity in relationship_target.iter() {
commands.entity(source_entity).despawn();
commands.entity(source_entity).try_despawn();
}
}
@ -323,6 +325,7 @@ pub enum RelationshipHookMode {
#[cfg(test)]
mod tests {
use crate::prelude::{ChildOf, Children};
use crate::world::World;
use crate::{component::Component, entity::Entity};
use alloc::vec::Vec;
@ -418,8 +421,6 @@ mod tests {
#[test]
fn parent_child_relationship_with_custom_relationship() {
use crate::prelude::ChildOf;
#[derive(Component)]
#[relationship(relationship_target = RelTarget)]
struct Rel(Entity);
@ -474,4 +475,34 @@ mod tests {
assert!(world.get_entity(child).is_err());
assert!(!world.entity(parent).contains::<RelTarget>());
}
#[test]
fn spawn_batch_with_relationship() {
let mut world = World::new();
let parent = world.spawn_empty().id();
let children = world
.spawn_batch((0..10).map(|_| ChildOf(parent)))
.collect::<Vec<_>>();
for &child in &children {
assert!(world
.get::<ChildOf>(child)
.is_some_and(|child_of| child_of.parent() == parent));
}
assert!(world
.get::<Children>(parent)
.is_some_and(|children| children.len() == 10));
}
#[test]
fn insert_batch_with_relationship() {
let mut world = World::new();
let parent = world.spawn_empty().id();
let child = world.spawn_empty().id();
world.insert_batch([(child, ChildOf(parent))]);
world.flush();
assert!(world.get::<ChildOf>(child).is_some());
assert!(world.get::<Children>(parent).is_some());
}
}

View File

@ -64,32 +64,23 @@ impl<const SEND: bool> ResourceData<SEND> {
/// If `SEND` is false, this will panic if called from a different thread than the one it was inserted from.
#[inline]
fn validate_access(&self) {
if SEND {
#[cfg_attr(
not(feature = "std"),
expect(
clippy::needless_return,
reason = "needless until no_std is addressed (see below)",
)
)]
return;
}
if !SEND {
#[cfg(feature = "std")]
if self.origin_thread_id != Some(std::thread::current().id()) {
// Panic in tests, as testing for aborting is nearly impossible
panic!(
"Attempted to access or drop non-send resource {} from thread {:?} on a thread {:?}. This is not allowed. Aborting.",
self.type_name,
self.origin_thread_id,
std::thread::current().id()
);
}
#[cfg(feature = "std")]
if self.origin_thread_id != Some(std::thread::current().id()) {
// Panic in tests, as testing for aborting is nearly impossible
panic!(
"Attempted to access or drop non-send resource {} from thread {:?} on a thread {:?}. This is not allowed. Aborting.",
self.type_name,
self.origin_thread_id,
std::thread::current().id()
);
// TODO: Handle no_std non-send.
// Currently, no_std is single-threaded only, so this is safe to ignore.
// To support no_std multithreading, an alternative will be required.
// Remove the #[expect] attribute above when this is addressed.
}
// TODO: Handle no_std non-send.
// Currently, no_std is single-threaded only, so this is safe to ignore.
// To support no_std multithreading, an alternative will be required.
// Remove the #[expect] attribute above when this is addressed.
}
/// Returns true if the resource is populated.

View File

@ -19,7 +19,7 @@ use crate::{
change_detection::{MaybeLocation, Mut},
component::{Component, ComponentId, Mutable},
entity::{Entities, Entity, EntityClonerBuilder, EntityDoesNotExistError, OptIn, OptOut},
error::{ignore, warn, BevyError, CommandWithEntity, ErrorContext, HandleError},
error::{warn, BevyError, CommandWithEntity, ErrorContext, HandleError},
event::{BufferedEvent, EntityEvent, Event},
observer::{Observer, TriggerTargets},
resource::Resource,
@ -641,6 +641,11 @@ impl<'w, 's> Commands<'w, 's> {
self.queue_internal(command.handle_error_with(error_handler));
}
/// Pushes a generic [`Command`] to the queue like [`Commands::queue_handled`], but instead silently ignores any errors.
pub fn queue_silenced<C: Command<T> + HandleError<T>, T>(&mut self, command: C) {
self.queue_internal(command.ignore_error());
}
fn queue_internal(&mut self, command: impl Command) {
match &mut self.queue {
InternalQueue::CommandQueue(queue) => {
@ -1466,12 +1471,11 @@ impl<'a> EntityCommands<'a> {
component_id: ComponentId,
value: T,
) -> &mut Self {
self.queue_handled(
self.queue_silenced(
// SAFETY:
// - `ComponentId` safety is ensured by the caller.
// - `T` safety is ensured by the caller.
unsafe { entity_command::insert_by_id(component_id, value, InsertMode::Replace) },
ignore,
)
}
@ -1523,7 +1527,7 @@ impl<'a> EntityCommands<'a> {
/// ```
#[track_caller]
pub fn try_insert(&mut self, bundle: impl Bundle) -> &mut Self {
self.queue_handled(entity_command::insert(bundle, InsertMode::Replace), ignore)
self.queue_silenced(entity_command::insert(bundle, InsertMode::Replace))
}
/// Adds a [`Bundle`] of components to the entity if the predicate returns true.
@ -1579,7 +1583,7 @@ impl<'a> EntityCommands<'a> {
/// the resulting error will be ignored.
#[track_caller]
pub fn try_insert_if_new(&mut self, bundle: impl Bundle) -> &mut Self {
self.queue_handled(entity_command::insert(bundle, InsertMode::Keep), ignore)
self.queue_silenced(entity_command::insert(bundle, InsertMode::Keep))
}
/// Removes a [`Bundle`] of components from the entity.
@ -1724,7 +1728,7 @@ impl<'a> EntityCommands<'a> {
/// # bevy_ecs::system::assert_is_system(remove_combat_stats_system);
/// ```
pub fn try_remove<B: Bundle>(&mut self) -> &mut Self {
self.queue_handled(entity_command::remove::<B>(), ignore)
self.queue_silenced(entity_command::remove::<B>())
}
/// Removes a [`Bundle`] of components from the entity,
@ -1818,7 +1822,7 @@ impl<'a> EntityCommands<'a> {
///
/// For example, this will recursively despawn [`Children`](crate::hierarchy::Children).
pub fn try_despawn(&mut self) {
self.queue_handled(entity_command::despawn(), ignore);
self.queue_silenced(entity_command::despawn());
}
/// Pushes an [`EntityCommand`] to the queue,
@ -1907,6 +1911,18 @@ impl<'a> EntityCommands<'a> {
self
}
/// Pushes an [`EntityCommand`] to the queue, which will get executed for the current [`Entity`].
///
/// Unlike [`EntityCommands::queue_handled`], this will completely ignore any errors that occur.
pub fn queue_silenced<C: EntityCommand<T> + CommandWithEntity<M>, T, M>(
&mut self,
command: C,
) -> &mut Self {
self.commands
.queue_silenced(command.with_entity(self.entity));
self
}
/// Removes all components except the given [`Bundle`] from the entity.
///
/// # Example

View File

@ -0,0 +1,42 @@
[package]
name = "bevy_feathers"
version = "0.17.0-dev"
edition = "2024"
description = "A collection of UI widgets for building editors and utilities in Bevy"
homepage = "https://bevy.org"
repository = "https://github.com/bevyengine/bevy"
license = "MIT OR Apache-2.0"
keywords = ["bevy"]
[dependencies]
# bevy
bevy_app = { path = "../bevy_app", version = "0.17.0-dev" }
bevy_asset = { path = "../bevy_asset", version = "0.17.0-dev" }
bevy_color = { path = "../bevy_color", version = "0.17.0-dev" }
bevy_core_widgets = { path = "../bevy_core_widgets", version = "0.17.0-dev" }
bevy_ecs = { path = "../bevy_ecs", version = "0.17.0-dev" }
bevy_input_focus = { path = "../bevy_input_focus", version = "0.17.0-dev" }
bevy_log = { path = "../bevy_log", version = "0.17.0-dev" }
bevy_math = { path = "../bevy_math", version = "0.17.0-dev" }
bevy_picking = { path = "../bevy_picking", version = "0.17.0-dev" }
bevy_platform = { path = "../bevy_platform", version = "0.17.0-dev" }
bevy_render = { path = "../bevy_render", version = "0.17.0-dev" }
bevy_text = { path = "../bevy_text", version = "0.17.0-dev" }
bevy_ui = { path = "../bevy_ui", version = "0.17.0-dev", features = [
"bevy_ui_picking_backend",
] }
bevy_window = { path = "../bevy_window", version = "0.17.0-dev" }
bevy_winit = { path = "../bevy_winit", version = "0.17.0-dev" }
# other
accesskit = "0.19"
[features]
default = []
[lints]
workspace = true
[package.metadata.docs.rs]
rustdoc-args = ["-Zunstable-options", "--generate-link-to-definition"]
all-features = true

View File

@ -0,0 +1,93 @@
Digitized data copyright (c) 2012-2015, The Mozilla Foundation and Telefonica S.A.
This Font Software is licensed under the SIL Open Font License, Version 1.1.
This license is copied below, and is also available with a FAQ at:
http://scripts.sil.org/OFL
-----------------------------------------------------------
SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
-----------------------------------------------------------
PREAMBLE
The goals of the Open Font License (OFL) are to stimulate worldwide
development of collaborative font projects, to support the font creation
efforts of academic and linguistic communities, and to provide a free and
open framework in which fonts may be shared and improved in partnership
with others.
The OFL allows the licensed fonts to be used, studied, modified and
redistributed freely as long as they are not sold by themselves. The
fonts, including any derivative works, can be bundled, embedded,
redistributed and/or sold with any software provided that any reserved
names are not used by derivative works. The fonts and derivatives,
however, cannot be released under any other type of license. The
requirement for fonts to remain under this license does not apply
to any document created using the fonts or their derivatives.
DEFINITIONS
"Font Software" refers to the set of files released by the Copyright
Holder(s) under this license and clearly marked as such. This may
include source files, build scripts and documentation.
"Reserved Font Name" refers to any names specified as such after the
copyright statement(s).
"Original Version" refers to the collection of Font Software components as
distributed by the Copyright Holder(s).
"Modified Version" refers to any derivative made by adding to, deleting,
or substituting -- in part or in whole -- any of the components of the
Original Version, by changing formats or by porting the Font Software to a
new environment.
"Author" refers to any designer, engineer, programmer, technical
writer or other person who contributed to the Font Software.
PERMISSION & CONDITIONS
Permission is hereby granted, free of charge, to any person obtaining
a copy of the Font Software, to use, study, copy, merge, embed, modify,
redistribute, and sell modified and unmodified copies of the Font
Software, subject to the following conditions:
1) Neither the Font Software nor any of its individual components,
in Original or Modified Versions, may be sold by itself.
2) Original or Modified Versions of the Font Software may be bundled,
redistributed and/or sold with any software, provided that each copy
contains the above copyright notice and this license. These can be
included either as stand-alone text files, human-readable headers or
in the appropriate machine-readable metadata fields within text or
binary files as long as those fields can be easily viewed by the user.
3) No Modified Version of the Font Software may use the Reserved Font
Name(s) unless explicit written permission is granted by the corresponding
Copyright Holder. This restriction only applies to the primary font name as
presented to the users.
4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
Software shall not be used to promote, endorse or advertise any
Modified Version, except to acknowledge the contribution(s) of the
Copyright Holder(s) and the Author(s) or with their explicit written
permission.
5) The Font Software, modified or unmodified, in part or in whole,
must be distributed entirely under this license, and must not be
distributed under any other license. The requirement for fonts to
remain under this license does not apply to any document created
using the Font Software.
TERMINATION
This license becomes null and void if any of the above conditions are
not met.
DISCLAIMER
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
OTHER DEALINGS IN THE FONT SOFTWARE.

Binary file not shown.

View File

@ -0,0 +1,93 @@
Copyright (c) 2012-2015, The Mozilla Foundation and Telefonica S.A.
This Font Software is licensed under the SIL Open Font License, Version 1.1.
This license is copied below, and is also available with a FAQ at:
https://openfontlicense.org
-----------------------------------------------------------
SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
-----------------------------------------------------------
PREAMBLE
The goals of the Open Font License (OFL) are to stimulate worldwide
development of collaborative font projects, to support the font creation
efforts of academic and linguistic communities, and to provide a free and
open framework in which fonts may be shared and improved in partnership
with others.
The OFL allows the licensed fonts to be used, studied, modified and
redistributed freely as long as they are not sold by themselves. The
fonts, including any derivative works, can be bundled, embedded,
redistributed and/or sold with any software provided that any reserved
names are not used by derivative works. The fonts and derivatives,
however, cannot be released under any other type of license. The
requirement for fonts to remain under this license does not apply
to any document created using the fonts or their derivatives.
DEFINITIONS
"Font Software" refers to the set of files released by the Copyright
Holder(s) under this license and clearly marked as such. This may
include source files, build scripts and documentation.
"Reserved Font Name" refers to any names specified as such after the
copyright statement(s).
"Original Version" refers to the collection of Font Software components as
distributed by the Copyright Holder(s).
"Modified Version" refers to any derivative made by adding to, deleting,
or substituting -- in part or in whole -- any of the components of the
Original Version, by changing formats or by porting the Font Software to a
new environment.
"Author" refers to any designer, engineer, programmer, technical
writer or other person who contributed to the Font Software.
PERMISSION & CONDITIONS
Permission is hereby granted, free of charge, to any person obtaining
a copy of the Font Software, to use, study, copy, merge, embed, modify,
redistribute, and sell modified and unmodified copies of the Font
Software, subject to the following conditions:
1) Neither the Font Software nor any of its individual components,
in Original or Modified Versions, may be sold by itself.
2) Original or Modified Versions of the Font Software may be bundled,
redistributed and/or sold with any software, provided that each copy
contains the above copyright notice and this license. These can be
included either as stand-alone text files, human-readable headers or
in the appropriate machine-readable metadata fields within text or
binary files as long as those fields can be easily viewed by the user.
3) No Modified Version of the Font Software may use the Reserved Font
Name(s) unless explicit written permission is granted by the corresponding
Copyright Holder. This restriction only applies to the primary font name as
presented to the users.
4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
Software shall not be used to promote, endorse or advertise any
Modified Version, except to acknowledge the contribution(s) of the
Copyright Holder(s) and the Author(s) or with their explicit written
permission.
5) The Font Software, modified or unmodified, in part or in whole,
must be distributed entirely under this license, and must not be
distributed under any other license. The requirement for fonts to
remain under this license does not apply to any document created
using the Font Software.
TERMINATION
This license becomes null and void if any of the above conditions are
not met.
DISCLAIMER
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
OTHER DEALINGS IN THE FONT SOFTWARE.

View File

@ -0,0 +1,29 @@
//! Various non-themable constants for the Feathers look and feel.
/// Font asset paths
pub mod fonts {
/// Default regular font path
pub const REGULAR: &str = "embedded://bevy_feathers/assets/fonts/FiraSans-Regular.ttf";
/// Regular italic font path
pub const ITALIC: &str = "embedded://bevy_feathers/assets/fonts/FiraSans-Italic.ttf";
/// Bold font path
pub const BOLD: &str = "embedded://bevy_feathers/assets/fonts/FiraSans-Bold.ttf";
/// Bold italic font path
pub const BOLD_ITALIC: &str = "embedded://bevy_feathers/assets/fonts/FiraSans-BoldItalic.ttf";
/// Monospace font path
pub const MONO: &str = "embedded://bevy_feathers/assets/fonts/FiraMono-Medium.ttf";
}
/// Size constants
pub mod size {
use bevy_ui::Val;
/// Common row size for buttons, sliders, spinners, etc.
pub const ROW_HEIGHT: Val = Val::Px(24.0);
/// Width and height of a checkbox
pub const CHECKBOX_SIZE: Val = Val::Px(18.0);
/// Width and height of a radio button
pub const RADIO_SIZE: Val = Val::Px(18.0);
}

View File

@ -0,0 +1,208 @@
use bevy_app::{Plugin, PreUpdate};
use bevy_core_widgets::{Callback, CoreButton};
use bevy_ecs::{
bundle::Bundle,
component::Component,
entity::Entity,
hierarchy::{ChildOf, Children},
lifecycle::RemovedComponents,
query::{Added, Changed, Has, Or},
schedule::IntoScheduleConfigs,
spawn::{SpawnRelated, SpawnableList},
system::{Commands, Query},
};
use bevy_input_focus::tab_navigation::TabIndex;
use bevy_picking::{hover::Hovered, PickingSystems};
use bevy_ui::{AlignItems, InteractionDisabled, JustifyContent, Node, Pressed, UiRect, Val};
use bevy_winit::cursor::CursorIcon;
use crate::{
constants::{fonts, size},
font_styles::InheritableFont,
handle_or_path::HandleOrPath,
rounded_corners::RoundedCorners,
theme::{ThemeBackgroundColor, ThemeFontColor},
tokens,
};
/// Color variants for buttons. This also functions as a component used by the dynamic styling
/// system to identify which entities are buttons.
#[derive(Component, Default, Clone)]
pub enum ButtonVariant {
/// The standard button appearance
#[default]
Normal,
/// A button with a more prominent color, this is used for "call to action" buttons,
/// default buttons for dialog boxes, and so on.
Primary,
}
/// Parameters for the button template, passed to [`button`] function.
#[derive(Default)]
pub struct ButtonProps {
/// Color variant for the button.
pub variant: ButtonVariant,
/// Rounded corners options
pub corners: RoundedCorners,
/// Click handler
pub on_click: Callback,
}
/// Template function to spawn a button.
///
/// # Arguments
/// * `props` - construction properties for the button.
/// * `overrides` - a bundle of components that are merged in with the normal button components.
/// * `children` - a [`SpawnableList`] of child elements, such as a label or icon for the button.
pub fn button<C: SpawnableList<ChildOf> + Send + Sync + 'static, B: Bundle>(
props: ButtonProps,
overrides: B,
children: C,
) -> impl Bundle {
(
Node {
height: size::ROW_HEIGHT,
justify_content: JustifyContent::Center,
align_items: AlignItems::Center,
padding: UiRect::axes(Val::Px(8.0), Val::Px(0.)),
flex_grow: 1.0,
..Default::default()
},
CoreButton {
on_activate: props.on_click,
},
props.variant,
Hovered::default(),
CursorIcon::System(bevy_window::SystemCursorIcon::Pointer),
TabIndex(0),
props.corners.to_border_radius(4.0),
ThemeBackgroundColor(tokens::BUTTON_BG),
ThemeFontColor(tokens::BUTTON_TEXT),
InheritableFont {
font: HandleOrPath::Path(fonts::REGULAR.to_owned()),
font_size: 14.0,
},
overrides,
Children::spawn(children),
)
}
fn update_button_styles(
q_buttons: Query<
(
Entity,
&ButtonVariant,
Has<InteractionDisabled>,
Has<Pressed>,
&Hovered,
&ThemeBackgroundColor,
&ThemeFontColor,
),
Or<(Changed<Hovered>, Added<Pressed>, Added<InteractionDisabled>)>,
>,
mut commands: Commands,
) {
for (button_ent, variant, disabled, pressed, hovered, bg_color, font_color) in q_buttons.iter()
{
set_button_colors(
button_ent,
variant,
disabled,
pressed,
hovered.0,
bg_color,
font_color,
&mut commands,
);
}
}
fn update_button_styles_remove(
q_buttons: Query<(
Entity,
&ButtonVariant,
Has<InteractionDisabled>,
Has<Pressed>,
&Hovered,
&ThemeBackgroundColor,
&ThemeFontColor,
)>,
mut removed_disabled: RemovedComponents<InteractionDisabled>,
mut removed_pressed: RemovedComponents<Pressed>,
mut commands: Commands,
) {
removed_disabled
.read()
.chain(removed_pressed.read())
.for_each(|ent| {
if let Ok((button_ent, variant, disabled, pressed, hovered, bg_color, font_color)) =
q_buttons.get(ent)
{
set_button_colors(
button_ent,
variant,
disabled,
pressed,
hovered.0,
bg_color,
font_color,
&mut commands,
);
}
});
}
fn set_button_colors(
button_ent: Entity,
variant: &ButtonVariant,
disabled: bool,
pressed: bool,
hovered: bool,
bg_color: &ThemeBackgroundColor,
font_color: &ThemeFontColor,
commands: &mut Commands,
) {
let bg_token = match (variant, disabled, pressed, hovered) {
(ButtonVariant::Normal, true, _, _) => tokens::BUTTON_BG_DISABLED,
(ButtonVariant::Normal, false, true, _) => tokens::BUTTON_BG_PRESSED,
(ButtonVariant::Normal, false, false, true) => tokens::BUTTON_BG_HOVER,
(ButtonVariant::Normal, false, false, false) => tokens::BUTTON_BG,
(ButtonVariant::Primary, true, _, _) => tokens::BUTTON_PRIMARY_BG_DISABLED,
(ButtonVariant::Primary, false, true, _) => tokens::BUTTON_PRIMARY_BG_PRESSED,
(ButtonVariant::Primary, false, false, true) => tokens::BUTTON_PRIMARY_BG_HOVER,
(ButtonVariant::Primary, false, false, false) => tokens::BUTTON_PRIMARY_BG,
};
let font_color_token = match (variant, disabled) {
(ButtonVariant::Normal, true) => tokens::BUTTON_TEXT_DISABLED,
(ButtonVariant::Normal, false) => tokens::BUTTON_TEXT,
(ButtonVariant::Primary, true) => tokens::BUTTON_PRIMARY_TEXT_DISABLED,
(ButtonVariant::Primary, false) => tokens::BUTTON_PRIMARY_TEXT,
};
// Change background color
if bg_color.0 != bg_token {
commands
.entity(button_ent)
.insert(ThemeBackgroundColor(bg_token));
}
// Change font color
if font_color.0 != font_color_token {
commands
.entity(button_ent)
.insert(ThemeFontColor(font_color_token));
}
}
/// Plugin which registers the systems for updating the button styles.
pub struct ButtonPlugin;
impl Plugin for ButtonPlugin {
fn build(&self, app: &mut bevy_app::App) {
app.add_systems(
PreUpdate,
(update_button_styles, update_button_styles_remove).in_set(PickingSystems::Last),
);
}
}

View File

@ -0,0 +1,304 @@
use bevy_app::{Plugin, PreUpdate};
use bevy_core_widgets::{Callback, CoreCheckbox};
use bevy_ecs::{
bundle::Bundle,
children,
component::Component,
entity::Entity,
hierarchy::{ChildOf, Children},
lifecycle::RemovedComponents,
query::{Added, Changed, Has, Or, With},
schedule::IntoScheduleConfigs,
spawn::{Spawn, SpawnRelated, SpawnableList},
system::{Commands, In, Query},
};
use bevy_input_focus::tab_navigation::TabIndex;
use bevy_math::Rot2;
use bevy_picking::{hover::Hovered, PickingSystems};
use bevy_render::view::Visibility;
use bevy_ui::{
AlignItems, BorderRadius, Checked, Display, FlexDirection, InteractionDisabled, JustifyContent,
Node, PositionType, UiRect, UiTransform, Val,
};
use bevy_winit::cursor::CursorIcon;
use crate::{
constants::{fonts, size},
font_styles::InheritableFont,
handle_or_path::HandleOrPath,
theme::{ThemeBackgroundColor, ThemeBorderColor, ThemeFontColor},
tokens,
};
/// Parameters for the checkbox template, passed to [`checkbox`] function.
#[derive(Default)]
pub struct CheckboxProps {
/// Change handler
pub on_change: Callback<In<bool>>,
}
/// Marker for the checkbox outline
#[derive(Component, Default, Clone)]
struct CheckboxOutline;
/// Marker for the checkbox check mark
#[derive(Component, Default, Clone)]
struct CheckboxMark;
/// Template function to spawn a checkbox.
///
/// # Arguments
/// * `props` - construction properties for the checkbox.
/// * `overrides` - a bundle of components that are merged in with the normal checkbox components.
/// * `label` - the label of the checkbox.
pub fn checkbox<C: SpawnableList<ChildOf> + Send + Sync + 'static, B: Bundle>(
props: CheckboxProps,
overrides: B,
label: C,
) -> impl Bundle {
(
Node {
display: Display::Flex,
flex_direction: FlexDirection::Row,
justify_content: JustifyContent::Start,
align_items: AlignItems::Center,
column_gap: Val::Px(4.0),
..Default::default()
},
CoreCheckbox {
on_change: props.on_change,
},
Hovered::default(),
CursorIcon::System(bevy_window::SystemCursorIcon::Pointer),
TabIndex(0),
ThemeFontColor(tokens::CHECKBOX_TEXT),
InheritableFont {
font: HandleOrPath::Path(fonts::REGULAR.to_owned()),
font_size: 14.0,
},
overrides,
Children::spawn((
Spawn((
Node {
width: size::CHECKBOX_SIZE,
height: size::CHECKBOX_SIZE,
border: UiRect::all(Val::Px(2.0)),
..Default::default()
},
CheckboxOutline,
BorderRadius::all(Val::Px(4.0)),
ThemeBackgroundColor(tokens::CHECKBOX_BG),
ThemeBorderColor(tokens::CHECKBOX_BORDER),
children![(
// Cheesy checkmark: rotated node with L-shaped border.
Node {
position_type: PositionType::Absolute,
left: Val::Px(4.0),
top: Val::Px(0.0),
width: Val::Px(6.),
height: Val::Px(11.),
border: UiRect {
bottom: Val::Px(2.0),
right: Val::Px(2.0),
..Default::default()
},
..Default::default()
},
UiTransform::from_rotation(Rot2::FRAC_PI_4),
CheckboxMark,
ThemeBorderColor(tokens::CHECKBOX_MARK),
)],
)),
label,
)),
)
}
fn update_checkbox_styles(
q_checkboxes: Query<
(
Entity,
Has<InteractionDisabled>,
Has<Checked>,
&Hovered,
&ThemeFontColor,
),
(
With<CoreCheckbox>,
Or<(Changed<Hovered>, Added<Checked>, Added<InteractionDisabled>)>,
),
>,
q_children: Query<&Children>,
mut q_outline: Query<(&ThemeBackgroundColor, &ThemeBorderColor), With<CheckboxOutline>>,
mut q_mark: Query<&ThemeBorderColor, With<CheckboxMark>>,
mut commands: Commands,
) {
for (checkbox_ent, disabled, checked, hovered, font_color) in q_checkboxes.iter() {
let Some(outline_ent) = q_children
.iter_descendants(checkbox_ent)
.find(|en| q_outline.contains(*en))
else {
continue;
};
let Some(mark_ent) = q_children
.iter_descendants(checkbox_ent)
.find(|en| q_mark.contains(*en))
else {
continue;
};
let (outline_bg, outline_border) = q_outline.get_mut(outline_ent).unwrap();
let mark_color = q_mark.get_mut(mark_ent).unwrap();
set_checkbox_colors(
checkbox_ent,
outline_ent,
mark_ent,
disabled,
checked,
hovered.0,
outline_bg,
outline_border,
mark_color,
font_color,
&mut commands,
);
}
}
fn update_checkbox_styles_remove(
q_checkboxes: Query<
(
Entity,
Has<InteractionDisabled>,
Has<Checked>,
&Hovered,
&ThemeFontColor,
),
With<CoreCheckbox>,
>,
q_children: Query<&Children>,
mut q_outline: Query<(&ThemeBackgroundColor, &ThemeBorderColor), With<CheckboxOutline>>,
mut q_mark: Query<&ThemeBorderColor, With<CheckboxMark>>,
mut removed_disabled: RemovedComponents<InteractionDisabled>,
mut removed_checked: RemovedComponents<Checked>,
mut commands: Commands,
) {
removed_disabled
.read()
.chain(removed_checked.read())
.for_each(|ent| {
if let Ok((checkbox_ent, disabled, checked, hovered, font_color)) =
q_checkboxes.get(ent)
{
let Some(outline_ent) = q_children
.iter_descendants(checkbox_ent)
.find(|en| q_outline.contains(*en))
else {
return;
};
let Some(mark_ent) = q_children
.iter_descendants(checkbox_ent)
.find(|en| q_mark.contains(*en))
else {
return;
};
let (outline_bg, outline_border) = q_outline.get_mut(outline_ent).unwrap();
let mark_color = q_mark.get_mut(mark_ent).unwrap();
set_checkbox_colors(
checkbox_ent,
outline_ent,
mark_ent,
disabled,
checked,
hovered.0,
outline_bg,
outline_border,
mark_color,
font_color,
&mut commands,
);
}
});
}
fn set_checkbox_colors(
checkbox_ent: Entity,
outline_ent: Entity,
mark_ent: Entity,
disabled: bool,
checked: bool,
hovered: bool,
outline_bg: &ThemeBackgroundColor,
outline_border: &ThemeBorderColor,
mark_color: &ThemeBorderColor,
font_color: &ThemeFontColor,
commands: &mut Commands,
) {
let outline_border_token = match (disabled, hovered) {
(true, _) => tokens::CHECKBOX_BORDER_DISABLED,
(false, true) => tokens::CHECKBOX_BORDER_HOVER,
_ => tokens::CHECKBOX_BORDER,
};
let outline_bg_token = match (disabled, checked) {
(true, true) => tokens::CHECKBOX_BG_CHECKED_DISABLED,
(true, false) => tokens::CHECKBOX_BG_DISABLED,
(false, true) => tokens::CHECKBOX_BG_CHECKED,
(false, false) => tokens::CHECKBOX_BG,
};
let mark_token = match disabled {
true => tokens::CHECKBOX_MARK_DISABLED,
false => tokens::CHECKBOX_MARK,
};
let font_color_token = match disabled {
true => tokens::CHECKBOX_TEXT_DISABLED,
false => tokens::CHECKBOX_TEXT,
};
// Change outline background
if outline_bg.0 != outline_bg_token {
commands
.entity(outline_ent)
.insert(ThemeBackgroundColor(outline_bg_token));
}
// Change outline border
if outline_border.0 != outline_border_token {
commands
.entity(outline_ent)
.insert(ThemeBorderColor(outline_border_token));
}
// Change mark color
if mark_color.0 != mark_token {
commands
.entity(mark_ent)
.insert(ThemeBorderColor(mark_token));
}
// Change mark visibility
commands.entity(mark_ent).insert(match checked {
true => Visibility::Visible,
false => Visibility::Hidden,
});
// Change font color
if font_color.0 != font_color_token {
commands
.entity(checkbox_ent)
.insert(ThemeFontColor(font_color_token));
}
}
/// Plugin which registers the systems for updating the checkbox styles.
pub struct CheckboxPlugin;
impl Plugin for CheckboxPlugin {
fn build(&self, app: &mut bevy_app::App) {
app.add_systems(
PreUpdate,
(update_checkbox_styles, update_checkbox_styles_remove).in_set(PickingSystems::Last),
);
}
}

View File

@ -0,0 +1,21 @@
//! Meta-module containing all feathers controls (widgets that are interactive).
use bevy_app::Plugin;
mod button;
mod checkbox;
mod radio;
mod slider;
pub use button::{button, ButtonPlugin, ButtonProps, ButtonVariant};
pub use checkbox::{checkbox, CheckboxPlugin, CheckboxProps};
pub use radio::{radio, RadioPlugin};
pub use slider::{slider, SliderPlugin, SliderProps};
/// Plugin which registers all `bevy_feathers` controls.
pub struct ControlsPlugin;
impl Plugin for ControlsPlugin {
fn build(&self, app: &mut bevy_app::App) {
app.add_plugins((ButtonPlugin, CheckboxPlugin, RadioPlugin, SliderPlugin));
}
}

View File

@ -0,0 +1,268 @@
use bevy_app::{Plugin, PreUpdate};
use bevy_core_widgets::CoreRadio;
use bevy_ecs::{
bundle::Bundle,
children,
component::Component,
entity::Entity,
hierarchy::{ChildOf, Children},
lifecycle::RemovedComponents,
query::{Added, Changed, Has, Or, With},
schedule::IntoScheduleConfigs,
spawn::{Spawn, SpawnRelated, SpawnableList},
system::{Commands, Query},
};
use bevy_input_focus::tab_navigation::TabIndex;
use bevy_picking::{hover::Hovered, PickingSystems};
use bevy_render::view::Visibility;
use bevy_ui::{
AlignItems, BorderRadius, Checked, Display, FlexDirection, InteractionDisabled, JustifyContent,
Node, UiRect, Val,
};
use bevy_winit::cursor::CursorIcon;
use crate::{
constants::{fonts, size},
font_styles::InheritableFont,
handle_or_path::HandleOrPath,
theme::{ThemeBackgroundColor, ThemeBorderColor, ThemeFontColor},
tokens,
};
/// Marker for the radio outline
#[derive(Component, Default, Clone)]
struct RadioOutline;
/// Marker for the radio check mark
#[derive(Component, Default, Clone)]
struct RadioMark;
/// Template function to spawn a radio.
///
/// # Arguments
/// * `props` - construction properties for the radio.
/// * `overrides` - a bundle of components that are merged in with the normal radio components.
/// * `label` - the label of the radio.
pub fn radio<C: SpawnableList<ChildOf> + Send + Sync + 'static, B: Bundle>(
overrides: B,
label: C,
) -> impl Bundle {
(
Node {
display: Display::Flex,
flex_direction: FlexDirection::Row,
justify_content: JustifyContent::Start,
align_items: AlignItems::Center,
column_gap: Val::Px(4.0),
..Default::default()
},
CoreRadio,
Hovered::default(),
CursorIcon::System(bevy_window::SystemCursorIcon::Pointer),
TabIndex(0),
ThemeFontColor(tokens::RADIO_TEXT),
InheritableFont {
font: HandleOrPath::Path(fonts::REGULAR.to_owned()),
font_size: 14.0,
},
overrides,
Children::spawn((
Spawn((
Node {
display: Display::Flex,
align_items: AlignItems::Center,
justify_content: JustifyContent::Center,
width: size::RADIO_SIZE,
height: size::RADIO_SIZE,
border: UiRect::all(Val::Px(2.0)),
..Default::default()
},
RadioOutline,
BorderRadius::MAX,
ThemeBorderColor(tokens::RADIO_BORDER),
children![(
// Cheesy checkmark: rotated node with L-shaped border.
Node {
width: Val::Px(8.),
height: Val::Px(8.),
..Default::default()
},
BorderRadius::MAX,
RadioMark,
ThemeBackgroundColor(tokens::RADIO_MARK),
)],
)),
label,
)),
)
}
fn update_radio_styles(
q_radioes: Query<
(
Entity,
Has<InteractionDisabled>,
Has<Checked>,
&Hovered,
&ThemeFontColor,
),
(
With<CoreRadio>,
Or<(Changed<Hovered>, Added<Checked>, Added<InteractionDisabled>)>,
),
>,
q_children: Query<&Children>,
mut q_outline: Query<&ThemeBorderColor, With<RadioOutline>>,
mut q_mark: Query<&ThemeBackgroundColor, With<RadioMark>>,
mut commands: Commands,
) {
for (radio_ent, disabled, checked, hovered, font_color) in q_radioes.iter() {
let Some(outline_ent) = q_children
.iter_descendants(radio_ent)
.find(|en| q_outline.contains(*en))
else {
continue;
};
let Some(mark_ent) = q_children
.iter_descendants(radio_ent)
.find(|en| q_mark.contains(*en))
else {
continue;
};
let outline_border = q_outline.get_mut(outline_ent).unwrap();
let mark_color = q_mark.get_mut(mark_ent).unwrap();
set_radio_colors(
radio_ent,
outline_ent,
mark_ent,
disabled,
checked,
hovered.0,
outline_border,
mark_color,
font_color,
&mut commands,
);
}
}
fn update_radio_styles_remove(
q_radioes: Query<
(
Entity,
Has<InteractionDisabled>,
Has<Checked>,
&Hovered,
&ThemeFontColor,
),
With<CoreRadio>,
>,
q_children: Query<&Children>,
mut q_outline: Query<&ThemeBorderColor, With<RadioOutline>>,
mut q_mark: Query<&ThemeBackgroundColor, With<RadioMark>>,
mut removed_disabled: RemovedComponents<InteractionDisabled>,
mut removed_checked: RemovedComponents<Checked>,
mut commands: Commands,
) {
removed_disabled
.read()
.chain(removed_checked.read())
.for_each(|ent| {
if let Ok((radio_ent, disabled, checked, hovered, font_color)) = q_radioes.get(ent) {
let Some(outline_ent) = q_children
.iter_descendants(radio_ent)
.find(|en| q_outline.contains(*en))
else {
return;
};
let Some(mark_ent) = q_children
.iter_descendants(radio_ent)
.find(|en| q_mark.contains(*en))
else {
return;
};
let outline_border = q_outline.get_mut(outline_ent).unwrap();
let mark_color = q_mark.get_mut(mark_ent).unwrap();
set_radio_colors(
radio_ent,
outline_ent,
mark_ent,
disabled,
checked,
hovered.0,
outline_border,
mark_color,
font_color,
&mut commands,
);
}
});
}
fn set_radio_colors(
radio_ent: Entity,
outline_ent: Entity,
mark_ent: Entity,
disabled: bool,
checked: bool,
hovered: bool,
outline_border: &ThemeBorderColor,
mark_color: &ThemeBackgroundColor,
font_color: &ThemeFontColor,
commands: &mut Commands,
) {
let outline_border_token = match (disabled, hovered) {
(true, _) => tokens::RADIO_BORDER_DISABLED,
(false, true) => tokens::RADIO_BORDER_HOVER,
_ => tokens::RADIO_BORDER,
};
let mark_token = match disabled {
true => tokens::RADIO_MARK_DISABLED,
false => tokens::RADIO_MARK,
};
let font_color_token = match disabled {
true => tokens::RADIO_TEXT_DISABLED,
false => tokens::RADIO_TEXT,
};
// Change outline border
if outline_border.0 != outline_border_token {
commands
.entity(outline_ent)
.insert(ThemeBorderColor(outline_border_token));
}
// Change mark color
if mark_color.0 != mark_token {
commands
.entity(mark_ent)
.insert(ThemeBorderColor(mark_token));
}
// Change mark visibility
commands.entity(mark_ent).insert(match checked {
true => Visibility::Visible,
false => Visibility::Hidden,
});
// Change font color
if font_color.0 != font_color_token {
commands
.entity(radio_ent)
.insert(ThemeFontColor(font_color_token));
}
}
/// Plugin which registers the systems for updating the radio styles.
pub struct RadioPlugin;
impl Plugin for RadioPlugin {
fn build(&self, app: &mut bevy_app::App) {
app.add_systems(
PreUpdate,
(update_radio_styles, update_radio_styles_remove).in_set(PickingSystems::Last),
);
}
}

View File

@ -0,0 +1,208 @@
use core::f32::consts::PI;
use bevy_app::{Plugin, PreUpdate};
use bevy_color::Color;
use bevy_core_widgets::{Callback, CoreSlider, SliderRange, SliderValue, TrackClick};
use bevy_ecs::{
bundle::Bundle,
children,
component::Component,
entity::Entity,
hierarchy::Children,
lifecycle::RemovedComponents,
query::{Added, Changed, Has, Or, Spawned, With},
schedule::IntoScheduleConfigs,
spawn::SpawnRelated,
system::{In, Query, Res},
};
use bevy_input_focus::tab_navigation::TabIndex;
use bevy_picking::PickingSystems;
use bevy_ui::{
widget::Text, AlignItems, BackgroundGradient, ColorStop, Display, FlexDirection, Gradient,
InteractionDisabled, InterpolationColorSpace, JustifyContent, LinearGradient, Node, UiRect,
Val,
};
use bevy_winit::cursor::CursorIcon;
use crate::{
constants::{fonts, size},
font_styles::InheritableFont,
handle_or_path::HandleOrPath,
rounded_corners::RoundedCorners,
theme::{ThemeFontColor, ThemedText, UiTheme},
tokens,
};
/// Slider template properties, passed to [`slider`] function.
pub struct SliderProps {
/// Slider current value
pub value: f32,
/// Slider minimum value
pub min: f32,
/// Slider maximum value
pub max: f32,
/// On-change handler
pub on_change: Callback<In<f32>>,
}
impl Default for SliderProps {
fn default() -> Self {
Self {
value: 0.0,
min: 0.0,
max: 1.0,
on_change: Callback::Ignore,
}
}
}
#[derive(Component, Default, Clone)]
#[require(CoreSlider)]
struct SliderStyle;
/// Marker for the text
#[derive(Component, Default, Clone)]
struct SliderValueText;
/// Spawn a new slider widget.
///
/// # Arguments
///
/// * `props` - construction properties for the slider.
/// * `overrides` - a bundle of components that are merged in with the normal slider components.
pub fn slider<B: Bundle>(props: SliderProps, overrides: B) -> impl Bundle {
(
Node {
height: size::ROW_HEIGHT,
justify_content: JustifyContent::Center,
align_items: AlignItems::Center,
padding: UiRect::axes(Val::Px(8.0), Val::Px(0.)),
flex_grow: 1.0,
..Default::default()
},
CoreSlider {
on_change: props.on_change,
track_click: TrackClick::Drag,
},
SliderStyle,
SliderValue(props.value),
SliderRange::new(props.min, props.max),
CursorIcon::System(bevy_window::SystemCursorIcon::EwResize),
TabIndex(0),
RoundedCorners::All.to_border_radius(6.0),
// Use a gradient to draw the moving bar
BackgroundGradient(vec![Gradient::Linear(LinearGradient {
angle: PI * 0.5,
stops: vec![
ColorStop::new(Color::NONE, Val::Percent(0.)),
ColorStop::new(Color::NONE, Val::Percent(50.)),
ColorStop::new(Color::NONE, Val::Percent(50.)),
ColorStop::new(Color::NONE, Val::Percent(100.)),
],
color_space: InterpolationColorSpace::Srgb,
})]),
overrides,
children![(
// Text container
Node {
display: Display::Flex,
flex_direction: FlexDirection::Row,
align_items: AlignItems::Center,
justify_content: JustifyContent::Center,
..Default::default()
},
ThemeFontColor(tokens::SLIDER_TEXT),
InheritableFont {
font: HandleOrPath::Path(fonts::MONO.to_owned()),
font_size: 12.0,
},
children![(Text::new("10.0"), ThemedText, SliderValueText,)],
)],
)
}
fn update_slider_colors(
mut q_sliders: Query<
(Has<InteractionDisabled>, &mut BackgroundGradient),
(With<SliderStyle>, Or<(Spawned, Added<InteractionDisabled>)>),
>,
theme: Res<UiTheme>,
) {
for (disabled, mut gradient) in q_sliders.iter_mut() {
set_slider_colors(&theme, disabled, gradient.as_mut());
}
}
fn update_slider_colors_remove(
mut q_sliders: Query<(Has<InteractionDisabled>, &mut BackgroundGradient)>,
mut removed_disabled: RemovedComponents<InteractionDisabled>,
theme: Res<UiTheme>,
) {
removed_disabled.read().for_each(|ent| {
if let Ok((disabled, mut gradient)) = q_sliders.get_mut(ent) {
set_slider_colors(&theme, disabled, gradient.as_mut());
}
});
}
fn set_slider_colors(theme: &Res<'_, UiTheme>, disabled: bool, gradient: &mut BackgroundGradient) {
let bar_color = theme.color(match disabled {
true => tokens::SLIDER_BAR_DISABLED,
false => tokens::SLIDER_BAR,
});
let bg_color = theme.color(tokens::SLIDER_BG);
if let [Gradient::Linear(linear_gradient)] = &mut gradient.0[..] {
linear_gradient.stops[0].color = bar_color;
linear_gradient.stops[1].color = bar_color;
linear_gradient.stops[2].color = bg_color;
linear_gradient.stops[3].color = bg_color;
}
}
fn update_slider_pos(
mut q_sliders: Query<
(Entity, &SliderValue, &SliderRange, &mut BackgroundGradient),
(
With<SliderStyle>,
Or<(
Changed<SliderValue>,
Changed<SliderRange>,
Changed<Children>,
)>,
),
>,
q_children: Query<&Children>,
mut q_slider_text: Query<&mut Text, With<SliderValueText>>,
) {
for (slider_ent, value, range, mut gradient) in q_sliders.iter_mut() {
if let [Gradient::Linear(linear_gradient)] = &mut gradient.0[..] {
let percent_value = range.thumb_position(value.0) * 100.0;
linear_gradient.stops[1].point = Val::Percent(percent_value);
linear_gradient.stops[2].point = Val::Percent(percent_value);
}
// Find slider text child entity and update its text with the formatted value
q_children.iter_descendants(slider_ent).for_each(|child| {
if let Ok(mut text) = q_slider_text.get_mut(child) {
text.0 = format!("{}", value.0);
}
});
}
}
/// Plugin which registers the systems for updating the slider styles.
pub struct SliderPlugin;
impl Plugin for SliderPlugin {
fn build(&self, app: &mut bevy_app::App) {
app.add_systems(
PreUpdate,
(
update_slider_colors,
update_slider_colors_remove,
update_slider_pos,
)
.in_set(PickingSystems::Last),
);
}
}

View File

@ -0,0 +1,70 @@
//! Provides a way to automatically set the mouse cursor based on hovered entity.
use bevy_app::{App, Plugin, PreUpdate};
use bevy_ecs::{
entity::Entity,
hierarchy::ChildOf,
resource::Resource,
schedule::IntoScheduleConfigs,
system::{Commands, Query, Res},
};
use bevy_picking::{hover::HoverMap, pointer::PointerId, PickingSystems};
use bevy_window::Window;
use bevy_winit::cursor::CursorIcon;
/// A component that specifies the cursor icon to be used when the mouse is not hovering over
/// any other entity. This is used to set the default cursor icon for the window.
#[derive(Resource, Debug, Clone, Default)]
pub struct DefaultCursorIcon(pub CursorIcon);
/// System which updates the window cursor icon whenever the mouse hovers over an entity with
/// a [`CursorIcon`] component. If no entity is hovered, the cursor icon is set to
/// the cursor in the [`DefaultCursorIcon`] resource.
pub(crate) fn update_cursor(
mut commands: Commands,
hover_map: Option<Res<HoverMap>>,
parent_query: Query<&ChildOf>,
cursor_query: Query<&CursorIcon>,
mut q_windows: Query<(Entity, &mut Window, Option<&CursorIcon>)>,
r_default_cursor: Res<DefaultCursorIcon>,
) {
let cursor = hover_map.and_then(|hover_map| match hover_map.get(&PointerId::Mouse) {
Some(hover_set) => hover_set.keys().find_map(|entity| {
cursor_query.get(*entity).ok().or_else(|| {
parent_query
.iter_ancestors(*entity)
.find_map(|e| cursor_query.get(e).ok())
})
}),
None => None,
});
let mut windows_to_change: Vec<Entity> = Vec::new();
for (entity, _window, prev_cursor) in q_windows.iter_mut() {
match (cursor, prev_cursor) {
(Some(cursor), Some(prev_cursor)) if cursor == prev_cursor => continue,
(None, None) => continue,
_ => {
windows_to_change.push(entity);
}
}
}
windows_to_change.iter().for_each(|entity| {
if let Some(cursor) = cursor {
commands.entity(*entity).insert(cursor.clone());
} else {
commands.entity(*entity).insert(r_default_cursor.0.clone());
}
});
}
/// Plugin that supports automatically changing the cursor based on the hovered entity.
pub struct CursorIconPlugin;
impl Plugin for CursorIconPlugin {
fn build(&self, app: &mut App) {
if app.world().get_resource::<DefaultCursorIcon>().is_none() {
app.init_resource::<DefaultCursorIcon>();
}
app.add_systems(PreUpdate, update_cursor.in_set(PickingSystems::Last));
}
}

View File

@ -0,0 +1,98 @@
//! The standard `bevy_feathers` dark theme.
use crate::{palette, tokens};
use bevy_color::{Alpha, Luminance};
use bevy_platform::collections::HashMap;
use crate::theme::ThemeProps;
/// Create a [`ThemeProps`] object and populate it with the colors for the default dark theme.
pub fn create_dark_theme() -> ThemeProps {
ThemeProps {
color: HashMap::from([
(tokens::WINDOW_BG.into(), palette::GRAY_0),
(tokens::BUTTON_BG.into(), palette::GRAY_3),
(
tokens::BUTTON_BG_HOVER.into(),
palette::GRAY_3.lighter(0.05),
),
(
tokens::BUTTON_BG_PRESSED.into(),
palette::GRAY_3.lighter(0.1),
),
(tokens::BUTTON_BG_DISABLED.into(), palette::GRAY_2),
(tokens::BUTTON_PRIMARY_BG.into(), palette::ACCENT),
(
tokens::BUTTON_PRIMARY_BG_HOVER.into(),
palette::ACCENT.lighter(0.05),
),
(
tokens::BUTTON_PRIMARY_BG_PRESSED.into(),
palette::ACCENT.lighter(0.1),
),
(tokens::BUTTON_PRIMARY_BG_DISABLED.into(), palette::GRAY_2),
(tokens::BUTTON_TEXT.into(), palette::WHITE),
(
tokens::BUTTON_TEXT_DISABLED.into(),
palette::WHITE.with_alpha(0.5),
),
(tokens::BUTTON_PRIMARY_TEXT.into(), palette::WHITE),
(
tokens::BUTTON_PRIMARY_TEXT_DISABLED.into(),
palette::WHITE.with_alpha(0.5),
),
(tokens::SLIDER_BG.into(), palette::GRAY_1),
(tokens::SLIDER_BAR.into(), palette::ACCENT),
(tokens::SLIDER_BAR_DISABLED.into(), palette::GRAY_2),
(tokens::SLIDER_TEXT.into(), palette::WHITE),
(
tokens::SLIDER_TEXT_DISABLED.into(),
palette::WHITE.with_alpha(0.5),
),
(tokens::CHECKBOX_BG.into(), palette::GRAY_3),
(tokens::CHECKBOX_BG_CHECKED.into(), palette::ACCENT),
(
tokens::CHECKBOX_BG_DISABLED.into(),
palette::GRAY_1.with_alpha(0.5),
),
(
tokens::CHECKBOX_BG_CHECKED_DISABLED.into(),
palette::GRAY_3.with_alpha(0.5),
),
(tokens::CHECKBOX_BORDER.into(), palette::GRAY_3),
(
tokens::CHECKBOX_BORDER_HOVER.into(),
palette::GRAY_3.lighter(0.1),
),
(
tokens::CHECKBOX_BORDER_DISABLED.into(),
palette::GRAY_3.with_alpha(0.5),
),
(tokens::CHECKBOX_MARK.into(), palette::WHITE),
(tokens::CHECKBOX_MARK_DISABLED.into(), palette::LIGHT_GRAY_2),
(tokens::CHECKBOX_TEXT.into(), palette::LIGHT_GRAY_1),
(
tokens::CHECKBOX_TEXT_DISABLED.into(),
palette::LIGHT_GRAY_1.with_alpha(0.5),
),
(tokens::RADIO_BORDER.into(), palette::GRAY_3),
(
tokens::RADIO_BORDER_HOVER.into(),
palette::GRAY_3.lighter(0.1),
),
(
tokens::RADIO_BORDER_DISABLED.into(),
palette::GRAY_3.with_alpha(0.5),
),
(tokens::RADIO_MARK.into(), palette::ACCENT),
(
tokens::RADIO_MARK_DISABLED.into(),
palette::ACCENT.with_alpha(0.5),
),
(tokens::RADIO_TEXT.into(), palette::LIGHT_GRAY_1),
(
tokens::RADIO_TEXT_DISABLED.into(),
palette::LIGHT_GRAY_1.with_alpha(0.5),
),
]),
}
}

View File

@ -0,0 +1,62 @@
//! A framework for inheritable font styles.
use bevy_app::Propagate;
use bevy_asset::{AssetServer, Handle};
use bevy_ecs::{
component::Component,
lifecycle::Insert,
observer::On,
system::{Commands, Query, Res},
};
use bevy_text::{Font, TextFont};
use crate::handle_or_path::HandleOrPath;
/// A component which, when inserted on an entity, will load the given font and propagate it
/// downward to any child text entity that has the [`ThemedText`](crate::theme::ThemedText) marker.
#[derive(Component, Default, Clone, Debug)]
pub struct InheritableFont {
/// The font handle or path.
pub font: HandleOrPath<Font>,
/// The desired font size.
pub font_size: f32,
}
impl InheritableFont {
/// Create a new `InheritableFont` from a handle.
pub fn from_handle(handle: Handle<Font>) -> Self {
Self {
font: HandleOrPath::Handle(handle),
font_size: 16.0,
}
}
/// Create a new `InheritableFont` from a path.
pub fn from_path(path: &str) -> Self {
Self {
font: HandleOrPath::Path(path.to_string()),
font_size: 16.0,
}
}
}
/// An observer which looks for changes to the `InheritableFont` component on an entity, and
/// propagates downward the font to all participating text entities.
pub(crate) fn on_changed_font(
ev: On<Insert, InheritableFont>,
font_style: Query<&InheritableFont>,
assets: Res<AssetServer>,
mut commands: Commands,
) {
if let Ok(style) = font_style.get(ev.target()) {
if let Some(font) = match style.font {
HandleOrPath::Handle(ref h) => Some(h.clone()),
HandleOrPath::Path(ref p) => Some(assets.load::<Font>(p)),
} {
commands.entity(ev.target()).insert(Propagate(TextFont {
font,
font_size: style.font_size,
..Default::default()
}));
}
}
}

View File

@ -0,0 +1,61 @@
//! Provides a way to specify assets either by handle or by path.
use bevy_asset::{Asset, Handle};
/// Enum that represents a reference to an asset as either a [`Handle`] or a [`String`] path.
///
/// This is useful for when you want to specify an asset, but don't always have convenient
/// access to an asset server reference.
#[derive(Clone, Debug)]
pub enum HandleOrPath<T: Asset> {
/// Specify the asset reference as a handle.
Handle(Handle<T>),
/// Specify the asset reference as a [`String`].
Path(String),
}
impl<T: Asset> Default for HandleOrPath<T> {
fn default() -> Self {
Self::Path("".to_string())
}
}
// Necessary because we don't want to require T: PartialEq
impl<T: Asset> PartialEq for HandleOrPath<T> {
fn eq(&self, other: &Self) -> bool {
match (self, other) {
(HandleOrPath::Handle(h1), HandleOrPath::Handle(h2)) => h1 == h2,
(HandleOrPath::Path(p1), HandleOrPath::Path(p2)) => p1 == p2,
_ => false,
}
}
}
impl<T: Asset> From<Handle<T>> for HandleOrPath<T> {
fn from(h: Handle<T>) -> Self {
HandleOrPath::Handle(h)
}
}
impl<T: Asset> From<&str> for HandleOrPath<T> {
fn from(p: &str) -> Self {
HandleOrPath::Path(p.to_string())
}
}
impl<T: Asset> From<String> for HandleOrPath<T> {
fn from(p: String) -> Self {
HandleOrPath::Path(p.clone())
}
}
impl<T: Asset> From<&String> for HandleOrPath<T> {
fn from(p: &String) -> Self {
HandleOrPath::Path(p.to_string())
}
}
impl<T: Asset + Clone> From<&HandleOrPath<T>> for HandleOrPath<T> {
fn from(p: &HandleOrPath<T>) -> Self {
p.to_owned()
}
}

View File

@ -0,0 +1,74 @@
//! `bevy_feathers` is a collection of styled and themed widgets for building editors and
//! inspectors.
//!
//! The aesthetic choices made here are designed with a future Bevy Editor in mind,
//! but this crate is deliberately exposed to the public to allow the broader ecosystem to easily create
//! tooling for themselves and others that fits cohesively together.
//!
//! While it may be tempting to use this crate for your game's UI, it's deliberately not intended for that.
//! We've opted for a clean, functional style, and prioritized consistency over customization.
//! That said, if you like what you see, it can be a helpful learning tool.
//! Consider copying this code into your own project,
//! and refining the styles and abstractions provided to meet your needs.
//!
//! ## Warning: Experimental!
//! All that said, this crate is still experimental and unfinished!
//! It will change in breaking ways, and there will be both bugs and limitations.
//!
//! Please report issues, submit fixes and propose changes.
//! Thanks for stress-testing; let's build something better together.
use bevy_app::{HierarchyPropagatePlugin, Plugin, PostUpdate};
use bevy_asset::embedded_asset;
use bevy_ecs::query::With;
use bevy_text::{TextColor, TextFont};
use bevy_winit::cursor::CursorIcon;
use crate::{
controls::ControlsPlugin,
cursor::{CursorIconPlugin, DefaultCursorIcon},
theme::{ThemedText, UiTheme},
};
pub mod constants;
pub mod controls;
pub mod cursor;
pub mod dark_theme;
pub mod font_styles;
pub mod handle_or_path;
pub mod palette;
pub mod rounded_corners;
pub mod theme;
pub mod tokens;
/// Plugin which installs observers and systems for feathers themes, cursors, and all controls.
pub struct FeathersPlugin;
impl Plugin for FeathersPlugin {
fn build(&self, app: &mut bevy_app::App) {
app.init_resource::<UiTheme>();
embedded_asset!(app, "assets/fonts/FiraSans-Bold.ttf");
embedded_asset!(app, "assets/fonts/FiraSans-BoldItalic.ttf");
embedded_asset!(app, "assets/fonts/FiraSans-Regular.ttf");
embedded_asset!(app, "assets/fonts/FiraSans-Italic.ttf");
embedded_asset!(app, "assets/fonts/FiraMono-Medium.ttf");
app.add_plugins((
ControlsPlugin,
CursorIconPlugin,
HierarchyPropagatePlugin::<TextColor, With<ThemedText>>::default(),
HierarchyPropagatePlugin::<TextFont, With<ThemedText>>::default(),
));
app.insert_resource(DefaultCursorIcon(CursorIcon::System(
bevy_window::SystemCursorIcon::Default,
)));
app.add_systems(PostUpdate, theme::update_theme)
.add_observer(theme::on_changed_background)
.add_observer(theme::on_changed_border)
.add_observer(theme::on_changed_font_color)
.add_observer(font_styles::on_changed_font);
}
}

View File

@ -0,0 +1,29 @@
//! The Feathers standard color palette.
use bevy_color::Color;
/// <div style="background-color: #000000; width: 10px; padding: 10px; border: 1px solid;"></div>
pub const BLACK: Color = Color::oklcha(0.0, 0.0, 0.0, 1.0);
/// <div style="background-color: #1F1F24; width: 10px; padding: 10px; border: 1px solid;"></div> - window background
pub const GRAY_0: Color = Color::oklcha(0.2414, 0.0095, 285.67, 1.0);
/// <div style="background-color: #2A2A2E; width: 10px; padding: 10px; border: 1px solid;"></div> - pane background
pub const GRAY_1: Color = Color::oklcha(0.2866, 0.0072, 285.93, 1.0);
/// <div style="background-color: #36373B; width: 10px; padding: 10px; border: 1px solid;"></div> - item background
pub const GRAY_2: Color = Color::oklcha(0.3373, 0.0071, 274.77, 1.0);
/// <div style="background-color: #46474D; width: 10px; padding: 10px; border: 1px solid;"></div> - item background (active)
pub const GRAY_3: Color = Color::oklcha(0.3992, 0.0101, 278.38, 1.0);
/// <div style="background-color: #414142; width: 10px; padding: 10px; border: 1px solid;"></div> - border
pub const WARM_GRAY_1: Color = Color::oklcha(0.3757, 0.0017, 286.32, 1.0);
/// <div style="background-color: #B1B1B2; width: 10px; padding: 10px; border: 1px solid;"></div> - bright label text
pub const LIGHT_GRAY_1: Color = Color::oklcha(0.7607, 0.0014, 286.37, 1.0);
/// <div style="background-color: #838385; width: 10px; padding: 10px; border: 1px solid;"></div> - dim label text
pub const LIGHT_GRAY_2: Color = Color::oklcha(0.6106, 0.003, 286.31, 1.0);
/// <div style="background-color: #FFFFFF; width: 10px; padding: 10px; border: 1px solid;"></div> - button label text
pub const WHITE: Color = Color::oklcha(1.0, 0.000000059604645, 90.0, 1.0);
/// <div style="background-color: #206EC9; width: 10px; padding: 10px; border: 1px solid;"></div> - call-to-action and selection color
pub const ACCENT: Color = Color::oklcha(0.542, 0.1594, 255.4, 1.0);
/// <div style="background-color: #AB4051; width: 10px; padding: 10px; border: 1px solid;"></div> - for X-axis inputs and drag handles
pub const X_AXIS: Color = Color::oklcha(0.5232, 0.1404, 13.84, 1.0);
/// <div style="background-color: #5D8D0A; width: 10px; padding: 10px; border: 1px solid;"></div> - for Y-axis inputs and drag handles
pub const Y_AXIS: Color = Color::oklcha(0.5866, 0.1543, 129.84, 1.0);
/// <div style="background-color: #2160A3; width: 10px; padding: 10px; border: 1px solid;"></div> - for Z-axis inputs and drag handles
pub const Z_AXIS: Color = Color::oklcha(0.4847, 0.1249, 253.08, 1.0);

View File

@ -0,0 +1,96 @@
//! Mechanism for specifying which corners of a widget are rounded, used for segmented buttons
//! and control groups.
use bevy_ui::{BorderRadius, Val};
/// Allows specifying which corners are rounded and which are sharp. All rounded corners
/// have the same radius. Not all combinations are supported, only the ones that make
/// sense for a segmented buttons.
///
/// A typical use case would be a segmented button consisting of 3 individual buttons in a
/// row. In that case, you would have the leftmost button have rounded corners on the left,
/// the right-most button have rounded corners on the right, and the center button have
/// only sharp corners.
#[derive(Debug, Clone, Copy, Default, PartialEq)]
pub enum RoundedCorners {
/// No corners are rounded.
None,
#[default]
/// All corners are rounded.
All,
/// Top-left corner is rounded.
TopLeft,
/// Top-right corner is rounded.
TopRight,
/// Bottom-right corner is rounded.
BottomRight,
/// Bottom-left corner is rounded.
BottomLeft,
/// Top corners are rounded.
Top,
/// Right corners are rounded.
Right,
/// Bottom corners are rounded.
Bottom,
/// Left corners are rounded.
Left,
}
impl RoundedCorners {
/// Convert the `RoundedCorners` to a `BorderRadius` for use in a `Node`.
pub fn to_border_radius(&self, radius: f32) -> BorderRadius {
let radius = Val::Px(radius);
let zero = Val::ZERO;
match self {
RoundedCorners::None => BorderRadius::all(zero),
RoundedCorners::All => BorderRadius::all(radius),
RoundedCorners::TopLeft => BorderRadius {
top_left: radius,
top_right: zero,
bottom_right: zero,
bottom_left: zero,
},
RoundedCorners::TopRight => BorderRadius {
top_left: zero,
top_right: radius,
bottom_right: zero,
bottom_left: zero,
},
RoundedCorners::BottomRight => BorderRadius {
top_left: zero,
top_right: zero,
bottom_right: radius,
bottom_left: zero,
},
RoundedCorners::BottomLeft => BorderRadius {
top_left: zero,
top_right: zero,
bottom_right: zero,
bottom_left: radius,
},
RoundedCorners::Top => BorderRadius {
top_left: radius,
top_right: radius,
bottom_right: zero,
bottom_left: zero,
},
RoundedCorners::Right => BorderRadius {
top_left: zero,
top_right: radius,
bottom_right: radius,
bottom_left: zero,
},
RoundedCorners::Bottom => BorderRadius {
top_left: zero,
top_right: zero,
bottom_right: radius,
bottom_left: radius,
},
RoundedCorners::Left => BorderRadius {
top_left: radius,
top_right: zero,
bottom_right: zero,
bottom_left: radius,
},
}
}
}

View File

@ -0,0 +1,131 @@
//! A framework for theming.
use bevy_app::Propagate;
use bevy_color::{palettes, Color};
use bevy_ecs::{
change_detection::DetectChanges,
component::Component,
lifecycle::Insert,
observer::On,
query::Changed,
resource::Resource,
system::{Commands, Query, Res},
};
use bevy_log::warn_once;
use bevy_platform::collections::HashMap;
use bevy_text::TextColor;
use bevy_ui::{BackgroundColor, BorderColor};
/// A collection of properties that make up a theme.
#[derive(Default, Clone)]
pub struct ThemeProps {
/// Map of design tokens to colors.
pub color: HashMap<String, Color>,
// Other style property types to be added later.
}
/// The currently selected user interface theme. Overwriting this resource changes the theme.
#[derive(Resource, Default)]
pub struct UiTheme(pub ThemeProps);
impl UiTheme {
/// Lookup a color by design token. If the theme does not have an entry for that token,
/// logs a warning and returns an error color.
pub fn color<'a>(&self, token: &'a str) -> Color {
let color = self.0.color.get(token);
match color {
Some(c) => *c,
None => {
warn_once!("Theme color {} not found.", token);
// Return a bright obnoxious color to make the error obvious.
palettes::basic::FUCHSIA.into()
}
}
}
/// Associate a design token with a given color.
pub fn set_color(&mut self, token: impl Into<String>, color: Color) {
self.0.color.insert(token.into(), color);
}
}
/// Component which causes the background color of an entity to be set based on a theme color.
#[derive(Component, Clone, Copy)]
#[require(BackgroundColor)]
#[component(immutable)]
pub struct ThemeBackgroundColor(pub &'static str);
/// Component which causes the border color of an entity to be set based on a theme color.
/// Only supports setting all borders to the same color.
#[derive(Component, Clone, Copy)]
#[require(BorderColor)]
#[component(immutable)]
pub struct ThemeBorderColor(pub &'static str);
/// Component which causes the inherited text color of an entity to be set based on a theme color.
#[derive(Component, Clone, Copy)]
#[component(immutable)]
pub struct ThemeFontColor(pub &'static str);
/// A marker component that is used to indicate that the text entity wants to opt-in to using
/// inherited text styles.
#[derive(Component)]
pub struct ThemedText;
pub(crate) fn update_theme(
mut q_background: Query<(&mut BackgroundColor, &ThemeBackgroundColor)>,
mut q_border: Query<(&mut BorderColor, &ThemeBorderColor)>,
theme: Res<UiTheme>,
) {
if theme.is_changed() {
// Update all background colors
for (mut bg, theme_bg) in q_background.iter_mut() {
bg.0 = theme.color(theme_bg.0);
}
// Update all border colors
for (mut border, theme_border) in q_border.iter_mut() {
border.set_all(theme.color(theme_border.0));
}
}
}
pub(crate) fn on_changed_background(
ev: On<Insert, ThemeBackgroundColor>,
mut q_background: Query<
(&mut BackgroundColor, &ThemeBackgroundColor),
Changed<ThemeBackgroundColor>,
>,
theme: Res<UiTheme>,
) {
// Update background colors where the design token has changed.
if let Ok((mut bg, theme_bg)) = q_background.get_mut(ev.target()) {
bg.0 = theme.color(theme_bg.0);
}
}
pub(crate) fn on_changed_border(
ev: On<Insert, ThemeBorderColor>,
mut q_border: Query<(&mut BorderColor, &ThemeBorderColor), Changed<ThemeBorderColor>>,
theme: Res<UiTheme>,
) {
// Update background colors where the design token has changed.
if let Ok((mut border, theme_border)) = q_border.get_mut(ev.target()) {
border.set_all(theme.color(theme_border.0));
}
}
/// An observer which looks for changes to the [`ThemeFontColor`] component on an entity, and
/// propagates downward the text color to all participating text entities.
pub(crate) fn on_changed_font_color(
ev: On<Insert, ThemeFontColor>,
font_color: Query<&ThemeFontColor>,
theme: Res<UiTheme>,
mut commands: Commands,
) {
if let Ok(token) = font_color.get(ev.target()) {
let color = theme.color(token.0);
commands
.entity(ev.target())
.insert(Propagate(TextColor(color)));
}
}

View File

@ -0,0 +1,101 @@
//! Design tokens used by Feathers themes.
//!
//! The term "design token" is commonly used in UX design to mean the smallest unit of a theme,
//! similar in concept to a CSS variable. Each token represents an assignment of a color or
//! value to a specific visual aspect of a widget, such as background or border.
/// Window background
pub const WINDOW_BG: &str = "feathers.window.bg";
/// Focus ring
pub const FOCUS_RING: &str = "feathers.focus";
/// Regular text
pub const TEXT_MAIN: &str = "feathers.text.main";
/// Dim text
pub const TEXT_DIM: &str = "feathers.text.dim";
// Normal buttons
/// Regular button background
pub const BUTTON_BG: &str = "feathers.button.bg";
/// Regular button background (hovered)
pub const BUTTON_BG_HOVER: &str = "feathers.button.bg.hover";
/// Regular button background (disabled)
pub const BUTTON_BG_DISABLED: &str = "feathers.button.bg.disabled";
/// Regular button background (pressed)
pub const BUTTON_BG_PRESSED: &str = "feathers.button.bg.pressed";
/// Regular button text
pub const BUTTON_TEXT: &str = "feathers.button.txt";
/// Regular button text (disabled)
pub const BUTTON_TEXT_DISABLED: &str = "feathers.button.txt.disabled";
// Primary ("default") buttons
/// Primary button background
pub const BUTTON_PRIMARY_BG: &str = "feathers.button.primary.bg";
/// Primary button background (hovered)
pub const BUTTON_PRIMARY_BG_HOVER: &str = "feathers.button.primary.bg.hover";
/// Primary button background (disabled)
pub const BUTTON_PRIMARY_BG_DISABLED: &str = "feathers.button.primary.bg.disabled";
/// Primary button background (pressed)
pub const BUTTON_PRIMARY_BG_PRESSED: &str = "feathers.button.primary.bg.pressed";
/// Primary button text
pub const BUTTON_PRIMARY_TEXT: &str = "feathers.button.primary.txt";
/// Primary button text (disabled)
pub const BUTTON_PRIMARY_TEXT_DISABLED: &str = "feathers.button.primary.txt.disabled";
// Slider
/// Background for slider
pub const SLIDER_BG: &str = "feathers.slider.bg";
/// Background for slider moving bar
pub const SLIDER_BAR: &str = "feathers.slider.bar";
/// Background for slider moving bar (disabled)
pub const SLIDER_BAR_DISABLED: &str = "feathers.slider.bar.disabled";
/// Background for slider text
pub const SLIDER_TEXT: &str = "feathers.slider.text";
/// Background for slider text (disabled)
pub const SLIDER_TEXT_DISABLED: &str = "feathers.slider.text.disabled";
// Checkbox
/// Checkbox background around the checkmark
pub const CHECKBOX_BG: &str = "feathers.checkbox.bg";
/// Checkbox border around the checkmark (disabled)
pub const CHECKBOX_BG_DISABLED: &str = "feathers.checkbox.bg.disabled";
/// Checkbox background around the checkmark
pub const CHECKBOX_BG_CHECKED: &str = "feathers.checkbox.bg.checked";
/// Checkbox border around the checkmark (disabled)
pub const CHECKBOX_BG_CHECKED_DISABLED: &str = "feathers.checkbox.bg.checked.disabled";
/// Checkbox border around the checkmark
pub const CHECKBOX_BORDER: &str = "feathers.checkbox.border";
/// Checkbox border around the checkmark (hovered)
pub const CHECKBOX_BORDER_HOVER: &str = "feathers.checkbox.border.hover";
/// Checkbox border around the checkmark (disabled)
pub const CHECKBOX_BORDER_DISABLED: &str = "feathers.checkbox.border.disabled";
/// Checkbox check mark
pub const CHECKBOX_MARK: &str = "feathers.checkbox.mark";
/// Checkbox check mark (disabled)
pub const CHECKBOX_MARK_DISABLED: &str = "feathers.checkbox.mark.disabled";
/// Checkbox label text
pub const CHECKBOX_TEXT: &str = "feathers.checkbox.text";
/// Checkbox label text (disabled)
pub const CHECKBOX_TEXT_DISABLED: &str = "feathers.checkbox.text.disabled";
// Radio button
/// Radio border around the checkmark
pub const RADIO_BORDER: &str = "feathers.radio.border";
/// Radio border around the checkmark (hovered)
pub const RADIO_BORDER_HOVER: &str = "feathers.radio.border.hover";
/// Radio border around the checkmark (disabled)
pub const RADIO_BORDER_DISABLED: &str = "feathers.radio.border.disabled";
/// Radio check mark
pub const RADIO_MARK: &str = "feathers.radio.mark";
/// Radio check mark (disabled)
pub const RADIO_MARK_DISABLED: &str = "feathers.radio.mark.disabled";
/// Radio label text
pub const RADIO_TEXT: &str = "feathers.radio.text";
/// Radio label text (disabled)
pub const RADIO_TEXT_DISABLED: &str = "feathers.radio.text.disabled";

View File

@ -124,7 +124,7 @@ use {
},
renderer::RenderDevice,
sync_world::{MainEntity, TemporaryRenderEntity},
Extract, ExtractSchedule, Render, RenderApp, RenderSystems,
Extract, ExtractSchedule, Render, RenderApp, RenderStartup, RenderSystems,
},
bytemuck::cast_slice,
};
@ -176,6 +176,8 @@ impl Plugin for GizmoPlugin {
#[cfg(feature = "bevy_render")]
if let Some(render_app) = app.get_sub_app_mut(RenderApp) {
render_app.add_systems(RenderStartup, init_line_gizmo_uniform_bind_group_layout);
render_app.add_systems(
Render,
prepare_line_gizmo_bind_group.in_set(RenderSystems::PrepareBindGroups),
@ -199,26 +201,6 @@ impl Plugin for GizmoPlugin {
tracing::warn!("bevy_render feature is enabled but RenderApp was not detected. Are you sure you loaded GizmoPlugin after RenderPlugin?");
}
}
#[cfg(feature = "bevy_render")]
fn finish(&self, app: &mut App) {
let Some(render_app) = app.get_sub_app_mut(RenderApp) else {
return;
};
let render_device = render_app.world().resource::<RenderDevice>();
let line_layout = render_device.create_bind_group_layout(
"LineGizmoUniform layout",
&BindGroupLayoutEntries::single(
ShaderStages::VERTEX,
uniform_buffer::<LineGizmoUniform>(true),
),
);
render_app.insert_resource(LineGizmoUniformBindgroupLayout {
layout: line_layout,
});
}
}
/// A extension trait adding `App::init_gizmo_group` and `App::insert_gizmo_config`.
@ -415,6 +397,24 @@ fn update_gizmo_meshes<Config: GizmoConfigGroup>(
}
}
#[cfg(feature = "bevy_render")]
fn init_line_gizmo_uniform_bind_group_layout(
mut commands: Commands,
render_device: Res<RenderDevice>,
) {
let line_layout = render_device.create_bind_group_layout(
"LineGizmoUniform layout",
&BindGroupLayoutEntries::single(
ShaderStages::VERTEX,
uniform_buffer::<LineGizmoUniform>(true),
),
);
commands.insert_resource(LineGizmoUniformBindgroupLayout {
layout: line_layout,
});
}
#[cfg(feature = "bevy_render")]
fn extract_gizmo_data(
mut commands: Commands,

View File

@ -1,23 +1,21 @@
use crate::{
config::{GizmoLineJoint, GizmoLineStyle, GizmoMeshConfig},
line_gizmo_vertex_buffer_layouts, line_joint_gizmo_vertex_buffer_layouts, DrawLineGizmo,
DrawLineJointGizmo, GizmoRenderSystems, GpuLineGizmo, LineGizmoUniformBindgroupLayout,
SetLineGizmoBindGroup,
init_line_gizmo_uniform_bind_group_layout, line_gizmo_vertex_buffer_layouts,
line_joint_gizmo_vertex_buffer_layouts, DrawLineGizmo, DrawLineJointGizmo, GizmoRenderSystems,
GpuLineGizmo, LineGizmoUniformBindgroupLayout, SetLineGizmoBindGroup,
};
use bevy_app::{App, Plugin};
use bevy_asset::{load_embedded_asset, Handle};
use bevy_asset::{load_embedded_asset, AssetServer, Handle};
use bevy_core_pipeline::core_2d::{Transparent2d, CORE_2D_DEPTH_FORMAT};
use bevy_ecs::{
prelude::Entity,
resource::Resource,
schedule::IntoScheduleConfigs,
system::{Query, Res, ResMut},
world::{FromWorld, World},
system::{Commands, Query, Res, ResMut},
};
use bevy_image::BevyDefault as _;
use bevy_math::FloatOrd;
use bevy_render::sync_world::MainEntity;
use bevy_render::{
render_asset::{prepare_assets, RenderAssets},
render_phase::{
@ -28,7 +26,9 @@ use bevy_render::{
view::{ExtractedView, Msaa, RenderLayers, ViewTarget},
Render, RenderApp, RenderSystems,
};
use bevy_render::{sync_world::MainEntity, RenderStartup};
use bevy_sprite::{Mesh2dPipeline, Mesh2dPipelineKey, SetMesh2dViewBindGroup};
use bevy_utils::default;
use tracing::error;
pub struct LineGizmo2dPlugin;
@ -54,6 +54,10 @@ impl Plugin for LineGizmo2dPlugin {
bevy_sprite::queue_material2d_meshes::<bevy_sprite::ColorMaterial>,
),
)
.add_systems(
RenderStartup,
init_line_gizmo_pipelines.after(init_line_gizmo_uniform_bind_group_layout),
)
.add_systems(
Render,
(queue_line_gizmos_2d, queue_line_joint_gizmos_2d)
@ -61,15 +65,6 @@ impl Plugin for LineGizmo2dPlugin {
.after(prepare_assets::<GpuLineGizmo>),
);
}
fn finish(&self, app: &mut App) {
let Some(render_app) = app.get_sub_app_mut(RenderApp) else {
return;
};
render_app.init_resource::<LineGizmoPipeline>();
render_app.init_resource::<LineJointGizmoPipeline>();
}
}
#[derive(Clone, Resource)]
@ -79,17 +74,22 @@ struct LineGizmoPipeline {
shader: Handle<Shader>,
}
impl FromWorld for LineGizmoPipeline {
fn from_world(render_world: &mut World) -> Self {
LineGizmoPipeline {
mesh_pipeline: render_world.resource::<Mesh2dPipeline>().clone(),
uniform_layout: render_world
.resource::<LineGizmoUniformBindgroupLayout>()
.layout
.clone(),
shader: load_embedded_asset!(render_world, "lines.wgsl"),
}
}
fn init_line_gizmo_pipelines(
mut commands: Commands,
mesh_2d_pipeline: Res<Mesh2dPipeline>,
uniform_bind_group_layout: Res<LineGizmoUniformBindgroupLayout>,
asset_server: Res<AssetServer>,
) {
commands.insert_resource(LineGizmoPipeline {
mesh_pipeline: mesh_2d_pipeline.clone(),
uniform_layout: uniform_bind_group_layout.layout.clone(),
shader: load_embedded_asset!(asset_server.as_ref(), "lines.wgsl"),
});
commands.insert_resource(LineJointGizmoPipeline {
mesh_pipeline: mesh_2d_pipeline.clone(),
uniform_layout: uniform_bind_group_layout.layout.clone(),
shader: load_embedded_asset!(asset_server.as_ref(), "line_joints.wgsl"),
});
}
#[derive(PartialEq, Eq, Hash, Clone)]
@ -128,14 +128,14 @@ impl SpecializedRenderPipeline for LineGizmoPipeline {
RenderPipelineDescriptor {
vertex: VertexState {
shader: self.shader.clone(),
entry_point: "vertex".into(),
shader_defs: shader_defs.clone(),
buffers: line_gizmo_vertex_buffer_layouts(key.strip),
..default()
},
fragment: Some(FragmentState {
shader: self.shader.clone(),
shader_defs,
entry_point: fragment_entry_point.into(),
entry_point: Some(fragment_entry_point.into()),
targets: vec![Some(ColorTargetState {
format,
blend: Some(BlendState::ALPHA_BLENDING),
@ -143,7 +143,6 @@ impl SpecializedRenderPipeline for LineGizmoPipeline {
})],
}),
layout,
primitive: PrimitiveState::default(),
depth_stencil: Some(DepthStencilState {
format: CORE_2D_DEPTH_FORMAT,
depth_write_enabled: false,
@ -166,8 +165,7 @@ impl SpecializedRenderPipeline for LineGizmoPipeline {
alpha_to_coverage_enabled: false,
},
label: Some("LineGizmo Pipeline 2D".into()),
push_constant_ranges: vec![],
zero_initialize_workgroup_memory: false,
..default()
}
}
}
@ -179,19 +177,6 @@ struct LineJointGizmoPipeline {
shader: Handle<Shader>,
}
impl FromWorld for LineJointGizmoPipeline {
fn from_world(render_world: &mut World) -> Self {
LineJointGizmoPipeline {
mesh_pipeline: render_world.resource::<Mesh2dPipeline>().clone(),
uniform_layout: render_world
.resource::<LineGizmoUniformBindgroupLayout>()
.layout
.clone(),
shader: load_embedded_asset!(render_world, "line_joints.wgsl"),
}
}
}
#[derive(PartialEq, Eq, Hash, Clone)]
struct LineJointGizmoPipelineKey {
mesh_key: Mesh2dPipelineKey,
@ -231,19 +216,19 @@ impl SpecializedRenderPipeline for LineJointGizmoPipeline {
RenderPipelineDescriptor {
vertex: VertexState {
shader: self.shader.clone(),
entry_point: entry_point.into(),
entry_point: Some(entry_point.into()),
shader_defs: shader_defs.clone(),
buffers: line_joint_gizmo_vertex_buffer_layouts(),
},
fragment: Some(FragmentState {
shader: self.shader.clone(),
shader_defs,
entry_point: "fragment".into(),
targets: vec![Some(ColorTargetState {
format,
blend: Some(BlendState::ALPHA_BLENDING),
write_mask: ColorWrites::ALL,
})],
..default()
}),
layout,
primitive: PrimitiveState::default(),
@ -269,8 +254,7 @@ impl SpecializedRenderPipeline for LineJointGizmoPipeline {
alpha_to_coverage_enabled: false,
},
label: Some("LineJointGizmo Pipeline 2D".into()),
push_constant_ranges: vec![],
zero_initialize_workgroup_memory: false,
..default()
}
}
}

View File

@ -1,11 +1,11 @@
use crate::{
config::{GizmoLineJoint, GizmoLineStyle, GizmoMeshConfig},
line_gizmo_vertex_buffer_layouts, line_joint_gizmo_vertex_buffer_layouts, DrawLineGizmo,
DrawLineJointGizmo, GizmoRenderSystems, GpuLineGizmo, LineGizmoUniformBindgroupLayout,
SetLineGizmoBindGroup,
init_line_gizmo_uniform_bind_group_layout, line_gizmo_vertex_buffer_layouts,
line_joint_gizmo_vertex_buffer_layouts, DrawLineGizmo, DrawLineJointGizmo, GizmoRenderSystems,
GpuLineGizmo, LineGizmoUniformBindgroupLayout, SetLineGizmoBindGroup,
};
use bevy_app::{App, Plugin};
use bevy_asset::{load_embedded_asset, Handle};
use bevy_asset::{load_embedded_asset, AssetServer, Handle};
use bevy_core_pipeline::{
core_3d::{Transparent3d, CORE_3D_DEPTH_FORMAT},
oit::OrderIndependentTransparencySettings,
@ -17,12 +17,10 @@ use bevy_ecs::{
query::Has,
resource::Resource,
schedule::IntoScheduleConfigs,
system::{Query, Res, ResMut},
world::{FromWorld, World},
system::{Commands, Query, Res, ResMut},
};
use bevy_image::BevyDefault as _;
use bevy_pbr::{MeshPipeline, MeshPipelineKey, SetMeshViewBindGroup};
use bevy_render::sync_world::MainEntity;
use bevy_render::{
render_asset::{prepare_assets, RenderAssets},
render_phase::{
@ -33,6 +31,8 @@ use bevy_render::{
view::{ExtractedView, Msaa, RenderLayers, ViewTarget},
Render, RenderApp, RenderSystems,
};
use bevy_render::{sync_world::MainEntity, RenderStartup};
use bevy_utils::default;
use tracing::error;
pub struct LineGizmo3dPlugin;
@ -50,9 +50,11 @@ impl Plugin for LineGizmo3dPlugin {
.init_resource::<SpecializedRenderPipelines<LineJointGizmoPipeline>>()
.configure_sets(
Render,
GizmoRenderSystems::QueueLineGizmos3d
.in_set(RenderSystems::Queue)
.ambiguous_with(bevy_pbr::queue_material_meshes::<bevy_pbr::StandardMaterial>),
GizmoRenderSystems::QueueLineGizmos3d.in_set(RenderSystems::Queue),
)
.add_systems(
RenderStartup,
init_line_gizmo_pipelines.after(init_line_gizmo_uniform_bind_group_layout),
)
.add_systems(
Render,
@ -61,15 +63,6 @@ impl Plugin for LineGizmo3dPlugin {
.after(prepare_assets::<GpuLineGizmo>),
);
}
fn finish(&self, app: &mut App) {
let Some(render_app) = app.get_sub_app_mut(RenderApp) else {
return;
};
render_app.init_resource::<LineGizmoPipeline>();
render_app.init_resource::<LineJointGizmoPipeline>();
}
}
#[derive(Clone, Resource)]
@ -79,17 +72,22 @@ struct LineGizmoPipeline {
shader: Handle<Shader>,
}
impl FromWorld for LineGizmoPipeline {
fn from_world(render_world: &mut World) -> Self {
LineGizmoPipeline {
mesh_pipeline: render_world.resource::<MeshPipeline>().clone(),
uniform_layout: render_world
.resource::<LineGizmoUniformBindgroupLayout>()
.layout
.clone(),
shader: load_embedded_asset!(render_world, "lines.wgsl"),
}
}
fn init_line_gizmo_pipelines(
mut commands: Commands,
mesh_pipeline: Res<MeshPipeline>,
uniform_bind_group_layout: Res<LineGizmoUniformBindgroupLayout>,
asset_server: Res<AssetServer>,
) {
commands.insert_resource(LineGizmoPipeline {
mesh_pipeline: mesh_pipeline.clone(),
uniform_layout: uniform_bind_group_layout.layout.clone(),
shader: load_embedded_asset!(asset_server.as_ref(), "lines.wgsl"),
});
commands.insert_resource(LineJointGizmoPipeline {
mesh_pipeline: mesh_pipeline.clone(),
uniform_layout: uniform_bind_group_layout.layout.clone(),
shader: load_embedded_asset!(asset_server.as_ref(), "line_joints.wgsl"),
});
}
#[derive(PartialEq, Eq, Hash, Clone)]
@ -134,14 +132,14 @@ impl SpecializedRenderPipeline for LineGizmoPipeline {
RenderPipelineDescriptor {
vertex: VertexState {
shader: self.shader.clone(),
entry_point: "vertex".into(),
shader_defs: shader_defs.clone(),
buffers: line_gizmo_vertex_buffer_layouts(key.strip),
..default()
},
fragment: Some(FragmentState {
shader: self.shader.clone(),
shader_defs,
entry_point: fragment_entry_point.into(),
entry_point: Some(fragment_entry_point.into()),
targets: vec![Some(ColorTargetState {
format,
blend: Some(BlendState::ALPHA_BLENDING),
@ -149,7 +147,6 @@ impl SpecializedRenderPipeline for LineGizmoPipeline {
})],
}),
layout,
primitive: PrimitiveState::default(),
depth_stencil: Some(DepthStencilState {
format: CORE_3D_DEPTH_FORMAT,
depth_write_enabled: true,
@ -163,8 +160,7 @@ impl SpecializedRenderPipeline for LineGizmoPipeline {
alpha_to_coverage_enabled: false,
},
label: Some("LineGizmo 3d Pipeline".into()),
push_constant_ranges: vec![],
zero_initialize_workgroup_memory: false,
..default()
}
}
}
@ -176,19 +172,6 @@ struct LineJointGizmoPipeline {
shader: Handle<Shader>,
}
impl FromWorld for LineJointGizmoPipeline {
fn from_world(render_world: &mut World) -> Self {
LineJointGizmoPipeline {
mesh_pipeline: render_world.resource::<MeshPipeline>().clone(),
uniform_layout: render_world
.resource::<LineGizmoUniformBindgroupLayout>()
.layout
.clone(),
shader: load_embedded_asset!(render_world, "line_joints.wgsl"),
}
}
}
#[derive(PartialEq, Eq, Hash, Clone)]
struct LineJointGizmoPipelineKey {
view_key: MeshPipelineKey,
@ -234,22 +217,21 @@ impl SpecializedRenderPipeline for LineJointGizmoPipeline {
RenderPipelineDescriptor {
vertex: VertexState {
shader: self.shader.clone(),
entry_point: entry_point.into(),
entry_point: Some(entry_point.into()),
shader_defs: shader_defs.clone(),
buffers: line_joint_gizmo_vertex_buffer_layouts(),
},
fragment: Some(FragmentState {
shader: self.shader.clone(),
shader_defs,
entry_point: "fragment".into(),
targets: vec![Some(ColorTargetState {
format,
blend: Some(BlendState::ALPHA_BLENDING),
write_mask: ColorWrites::ALL,
})],
..default()
}),
layout,
primitive: PrimitiveState::default(),
depth_stencil: Some(DepthStencilState {
format: CORE_3D_DEPTH_FORMAT,
depth_write_enabled: true,
@ -263,8 +245,7 @@ impl SpecializedRenderPipeline for LineJointGizmoPipeline {
alpha_to_coverage_enabled: false,
},
label: Some("LineJointGizmo 3d Pipeline".into()),
push_constant_ranges: vec![],
zero_initialize_workgroup_memory: false,
..default()
}
}
}

View File

@ -149,7 +149,7 @@ pub(crate) fn extract_linegizmos(
line_style: gizmo.line_config.style,
line_joints: gizmo.line_config.joints,
render_layers: render_layers.cloned().unwrap_or_default(),
handle: gizmo.handle.clone_weak(),
handle: gizmo.handle.clone(),
},
MainEntity::from(entity),
TemporaryRenderEntity,

View File

@ -15,6 +15,7 @@ pbr_multi_layer_material_textures = [
]
pbr_anisotropy_texture = ["bevy_pbr/pbr_anisotropy_texture"]
pbr_specular_textures = ["bevy_pbr/pbr_specular_textures"]
gltf_convert_coordinates_default = []
[dependencies]
# bevy
@ -64,8 +65,6 @@ serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0.140"
smallvec = "1.11"
tracing = { version = "0.1", default-features = false, features = ["std"] }
[dev-dependencies]
bevy_log = { path = "../bevy_log", version = "0.17.0-dev" }
[lints]

View File

@ -185,7 +185,7 @@ impl Default for GltfPlugin {
GltfPlugin {
default_sampler: ImageSamplerDescriptor::linear(),
custom_vertex_attributes: HashMap::default(),
convert_coordinates: false,
convert_coordinates: cfg!(feature = "gltf_convert_coordinates_default"),
}
}
}

View File

@ -2,6 +2,7 @@ mod extensions;
mod gltf_ext;
use alloc::sync::Arc;
use bevy_log::warn_once;
use std::{
io::Error,
path::{Path, PathBuf},
@ -297,7 +298,18 @@ async fn load_gltf<'a, 'b, 'c>(
let convert_coordinates = match settings.convert_coordinates {
Some(convert_coordinates) => convert_coordinates,
None => loader.default_convert_coordinates,
None => {
let convert_by_default = loader.default_convert_coordinates;
if !convert_by_default && !cfg!(feature = "gltf_convert_coordinates_default") {
warn_once!(
"Starting from Bevy 0.18, by default all imported glTF models will be rotated by 180 degrees around the Y axis to align with Bevy's coordinate system. \
You are currently importing glTF files using the old behavior. Consider opting-in to the new import behavior by enabling the `gltf_convert_coordinates_default` feature. \
If you encounter any issues please file a bug! \
If you want to continue using the old behavior going forward (even when the default changes in 0.18), manually set the corresponding option in the `GltfPlugin` or `GltfLoaderSettings`. See the migration guide for more details."
);
}
convert_by_default
}
};
#[cfg(feature = "bevy_animation")]

View File

@ -23,9 +23,9 @@ pub fn dds_buffer_to_image(
Ok(format) => (format, None),
Err(TextureError::FormatRequiresTranscodingError(TranscodeFormat::Rgb8)) => {
let format = if is_srgb {
TextureFormat::Bgra8UnormSrgb
TextureFormat::Rgba8UnormSrgb
} else {
TextureFormat::Bgra8Unorm
TextureFormat::Rgba8Unorm
};
(format, Some(TranscodeFormat::Rgb8))
}

View File

@ -1558,11 +1558,11 @@ pub enum DataFormat {
pub enum TranscodeFormat {
Etc1s,
Uastc(DataFormat),
// Has to be transcoded to R8Unorm for use with `wgpu`.
/// Has to be transcoded from `R8UnormSrgb` to `R8Unorm` for use with `wgpu`.
R8UnormSrgb,
// Has to be transcoded to R8G8Unorm for use with `wgpu`.
/// Has to be transcoded from `Rg8UnormSrgb` to `R8G8Unorm` for use with `wgpu`.
Rg8UnormSrgb,
// Has to be transcoded to Rgba8 for use with `wgpu`.
/// Has to be transcoded from `Rgb8` to `Rgba8` for use with `wgpu`.
Rgb8,
}

View File

@ -238,11 +238,16 @@ pub fn ktx2_buffer_to_image(
)));
}
// Collect all level data into a contiguous buffer
let mut image_data = Vec::new();
image_data.reserve_exact(levels.iter().map(Vec::len).sum());
levels.iter().for_each(|level| image_data.extend(level));
// Assign the data and fill in the rest of the metadata now the possible
// error cases have been handled
let mut image = Image::default();
image.texture_descriptor.format = texture_format;
image.data = Some(levels.into_iter().flatten().collect::<Vec<_>>());
image.data = Some(image_data);
image.data_order = wgpu_types::TextureDataOrder::MipMajor;
// Note: we must give wgpu the logical texture dimensions, so it can correctly compute mip sizes.
// However this currently causes wgpu to panic if the dimensions arent a multiple of blocksize.

Some files were not shown because too many files have changed in this diff Show More