Merge branch 'main' into render-diagnostics
This commit is contained in:
commit
cb159ef5b6
7
.github/workflows/ci.yml
vendored
7
.github/workflows/ci.yml
vendored
@ -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
|
||||
|
||||
64
Cargo.toml
64
Cargo.toml
@ -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
|
||||
|
||||
@ -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,
|
||||
),
|
||||
|
||||
BIN
assets/lightmaps/caustic_directional_texture.png
Normal file
BIN
assets/lightmaps/caustic_directional_texture.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 229 KiB |
BIN
assets/lightmaps/faces_pointlight_texture_blurred.png
Normal file
BIN
assets/lightmaps/faces_pointlight_texture_blurred.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 551 KiB |
BIN
assets/lightmaps/torch_spotlight_texture.png
Normal file
BIN
assets/lightmaps/torch_spotlight_texture.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 93 KiB |
BIN
assets/models/Faces/faces.glb
Normal file
BIN
assets/models/Faces/faces.glb
Normal file
Binary file not shown.
11
assets/shaders/manual_material.wgsl
Normal file
11
assets/shaders/manual_material.wgsl
Normal 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);
|
||||
}
|
||||
@ -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
|
||||
|
||||
67
benches/benches/bevy_ecs/bundles/insert_many.rs
Normal file
67
benches/benches/bevy_ecs/bundles/insert_many.rs
Normal 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();
|
||||
}
|
||||
14
benches/benches/bevy_ecs/bundles/mod.rs
Normal file
14
benches/benches/bevy_ecs/bundles/mod.rs
Normal 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,
|
||||
);
|
||||
40
benches/benches/bevy_ecs/bundles/spawn_many.rs
Normal file
40
benches/benches/bevy_ecs/bundles/spawn_many.rs
Normal 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();
|
||||
}
|
||||
27
benches/benches/bevy_ecs/bundles/spawn_many_zst.rs
Normal file
27
benches/benches/bevy_ecs/bundles/spawn_many_zst.rs
Normal 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();
|
||||
}
|
||||
24
benches/benches/bevy_ecs/bundles/spawn_one_zst.rs
Normal file
24
benches/benches/bevy_ecs/bundles/spawn_one_zst.rs
Normal 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();
|
||||
}
|
||||
@ -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,
|
||||
|
||||
@ -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" }
|
||||
|
||||
@ -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`].
|
||||
///
|
||||
|
||||
@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -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);
|
||||
}
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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(),
|
||||
});
|
||||
|
||||
@ -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,
|
||||
},
|
||||
|
||||
@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -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},
|
||||
|
||||
@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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) {
|
||||
|
||||
@ -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) {
|
||||
|
||||
@ -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: &[],
|
||||
},
|
||||
)
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -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(),
|
||||
}
|
||||
}
|
||||
|
||||
@ -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,
|
||||
};
|
||||
|
||||
@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -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},
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -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"]
|
||||
|
||||
113
crates/bevy_core_widgets/src/callback.rs
Normal file
113
crates/bevy_core_widgets/src/callback.rs
Normal 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 => (),
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
329
crates/bevy_core_widgets/src/core_scrollbar.rs
Normal file
329
crates/bevy_core_widgets/src/core_scrollbar.rs
Normal 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);
|
||||
}
|
||||
}
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -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,
|
||||
));
|
||||
}
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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() {
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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());
|
||||
}
|
||||
}
|
||||
|
||||
@ -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.
|
||||
|
||||
@ -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
|
||||
|
||||
42
crates/bevy_feathers/Cargo.toml
Normal file
42
crates/bevy_feathers/Cargo.toml
Normal 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
|
||||
93
crates/bevy_feathers/src/assets/fonts/FiraMono-LICENSE
Normal file
93
crates/bevy_feathers/src/assets/fonts/FiraMono-LICENSE
Normal 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.
|
||||
BIN
crates/bevy_feathers/src/assets/fonts/FiraMono-Medium.ttf
Executable file
BIN
crates/bevy_feathers/src/assets/fonts/FiraMono-Medium.ttf
Executable file
Binary file not shown.
BIN
crates/bevy_feathers/src/assets/fonts/FiraSans-Bold.ttf
Normal file
BIN
crates/bevy_feathers/src/assets/fonts/FiraSans-Bold.ttf
Normal file
Binary file not shown.
BIN
crates/bevy_feathers/src/assets/fonts/FiraSans-BoldItalic.ttf
Normal file
BIN
crates/bevy_feathers/src/assets/fonts/FiraSans-BoldItalic.ttf
Normal file
Binary file not shown.
BIN
crates/bevy_feathers/src/assets/fonts/FiraSans-Italic.ttf
Normal file
BIN
crates/bevy_feathers/src/assets/fonts/FiraSans-Italic.ttf
Normal file
Binary file not shown.
93
crates/bevy_feathers/src/assets/fonts/FiraSans-License.txt
Normal file
93
crates/bevy_feathers/src/assets/fonts/FiraSans-License.txt
Normal 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.
|
||||
BIN
crates/bevy_feathers/src/assets/fonts/FiraSans-Regular.ttf
Normal file
BIN
crates/bevy_feathers/src/assets/fonts/FiraSans-Regular.ttf
Normal file
Binary file not shown.
29
crates/bevy_feathers/src/constants.rs
Normal file
29
crates/bevy_feathers/src/constants.rs
Normal 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);
|
||||
}
|
||||
208
crates/bevy_feathers/src/controls/button.rs
Normal file
208
crates/bevy_feathers/src/controls/button.rs
Normal 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),
|
||||
);
|
||||
}
|
||||
}
|
||||
304
crates/bevy_feathers/src/controls/checkbox.rs
Normal file
304
crates/bevy_feathers/src/controls/checkbox.rs
Normal 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),
|
||||
);
|
||||
}
|
||||
}
|
||||
21
crates/bevy_feathers/src/controls/mod.rs
Normal file
21
crates/bevy_feathers/src/controls/mod.rs
Normal 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));
|
||||
}
|
||||
}
|
||||
268
crates/bevy_feathers/src/controls/radio.rs
Normal file
268
crates/bevy_feathers/src/controls/radio.rs
Normal 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),
|
||||
);
|
||||
}
|
||||
}
|
||||
208
crates/bevy_feathers/src/controls/slider.rs
Normal file
208
crates/bevy_feathers/src/controls/slider.rs
Normal 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),
|
||||
);
|
||||
}
|
||||
}
|
||||
70
crates/bevy_feathers/src/cursor.rs
Normal file
70
crates/bevy_feathers/src/cursor.rs
Normal 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));
|
||||
}
|
||||
}
|
||||
98
crates/bevy_feathers/src/dark_theme.rs
Normal file
98
crates/bevy_feathers/src/dark_theme.rs
Normal 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),
|
||||
),
|
||||
]),
|
||||
}
|
||||
}
|
||||
62
crates/bevy_feathers/src/font_styles.rs
Normal file
62
crates/bevy_feathers/src/font_styles.rs
Normal 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()
|
||||
}));
|
||||
}
|
||||
}
|
||||
}
|
||||
61
crates/bevy_feathers/src/handle_or_path.rs
Normal file
61
crates/bevy_feathers/src/handle_or_path.rs
Normal 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()
|
||||
}
|
||||
}
|
||||
74
crates/bevy_feathers/src/lib.rs
Normal file
74
crates/bevy_feathers/src/lib.rs
Normal 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);
|
||||
}
|
||||
}
|
||||
29
crates/bevy_feathers/src/palette.rs
Normal file
29
crates/bevy_feathers/src/palette.rs
Normal 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);
|
||||
96
crates/bevy_feathers/src/rounded_corners.rs
Normal file
96
crates/bevy_feathers/src/rounded_corners.rs
Normal 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,
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
131
crates/bevy_feathers/src/theme.rs
Normal file
131
crates/bevy_feathers/src/theme.rs
Normal 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)));
|
||||
}
|
||||
}
|
||||
101
crates/bevy_feathers/src/tokens.rs
Normal file
101
crates/bevy_feathers/src/tokens.rs
Normal 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";
|
||||
@ -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,
|
||||
|
||||
@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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]
|
||||
|
||||
@ -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"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -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")]
|
||||
|
||||
@ -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))
|
||||
}
|
||||
|
||||
@ -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,
|
||||
}
|
||||
|
||||
|
||||
@ -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
Loading…
Reference in New Issue
Block a user