Vendor dependencies for 0.3.0 release

This commit is contained in:
2025-09-27 10:29:08 -05:00
parent 0c8d39d483
commit 82ab7f317b
26803 changed files with 16134934 additions and 0 deletions

1
vendor/bevy_ui/.cargo-checksum.json vendored Normal file
View File

@@ -0,0 +1 @@
{"files":{"Cargo.lock":"7e47015a97336b998e39e755c8a6f97e4116b899f487a4d933f13722246594ab","Cargo.toml":"8458af74670e705ceed2f2025643ff01143edd80b0241f7dae790fd85e452df6","LICENSE-APACHE":"a6cba85bc92e0cff7a450b1d873c0eaa2e9fc96bf472df0247a26bec77bf3ff9","LICENSE-MIT":"508a77d2e7b51d98adeed32648ad124b7b30241a8e70b2e72c99f92d8e5874d1","README.md":"3510961f951e0a7648c58d8a935a28ee593caa70997c8e34178290d0bd35c1a5","src/accessibility.rs":"9b5a2461a083254cb76f35d7eaee9bd6e140a357b7f059dad11e3167ddb0ca25","src/experimental/ghost_hierarchy.rs":"63970d62d9449877e4b3782d0f16d10751ae42486fcc0063442036945470b96e","src/experimental/mod.rs":"62df513a02f16e3d950059d8492035993a0508b5c597dc95f2bca464612c629e","src/focus.rs":"205537f0258951cd570c2cca42458874cbc768e5935fa75d2c3771bb2a39f19c","src/geometry.rs":"bcf7b3af404c29fe3139bf0d9621a7ce7a3149aa51a53b431be700ce5ec8ff40","src/layout/convert.rs":"affca884cd20eabaca2d7c96304e541624ba03692f1fe7616f19f4526b2369e6","src/layout/debug.rs":"787ec59403139fd73c49ac83e723d3429c2e6bc195d7373783574928e362cbd7","src/layout/mod.rs":"11a753893e78ded3168cd4faa069b8cb9bf7882d864c66b2671d5deb91e9010f","src/layout/ui_surface.rs":"249fc9df57803b51986af019104d5bb7e2338f4f244a920a21d7a4b689715d6a","src/lib.rs":"fba3f267f67493544e5d5ac3bcf829c0a9d298dd973bcc257e291ceae503d68c","src/measurement.rs":"cb081fdaa5e35ff44e3c7b270178cdb982df7697189b4405b884d7da187d5396","src/picking_backend.rs":"38387390b52d1079e41c36958b360cab08dba934e8db06c0b5d4ad0aa3e653bf","src/render/box_shadow.rs":"251d61cf30c75e2241a05167e5bc48a1d7c98ce1705ac0688ef8acfbb9bf675a","src/render/box_shadow.wgsl":"f6ec9afa7bca3a90ac962865f4b27a7452bc693fccb0a64a516fdb88dd4d08cf","src/render/debug_overlay.rs":"73733a70c75434e085266e426bc8d72bdbf5bea0edafcfe05c6d380ba10e29a5","src/render/mod.rs":"b7e46dde458f9d41395ed47bc7f38b84461672ee66804ee8cde8b17fd399b1b9","src/render/pipeline.rs":"482f79540b2d19160e9670f35a61ad8d89745d81ac4bc9c9bafaa2eaf0ec2c81","src/render/render_pass.rs":"a1f119a95a2405c6d4b85157e706cb45221c7e6f84bbbdf978f13e9b71c62a38","src/render/ui.wgsl":"a4521dd1910701d3870681f9e9dd69326781c31cfc7849af2fec8624967a0336","src/render/ui_material.wgsl":"35c8a52e9518e66847d3b46c53201e613b9c7e2ec47b57792cbf8c924f97c7fe","src/render/ui_material_pipeline.rs":"2ea02ae75fae3a6c7f19c4c80486785813685744cdf35324389df85498fc77a7","src/render/ui_texture_slice.wgsl":"226f2f35161cd2da2df4cdc72a58972cd3c162fb7af5898350d3271ba8385f00","src/render/ui_texture_slice_pipeline.rs":"05f1a80dbaaeb2fd7d5e494c8d0bb45a0884a6106d5e6a11e67500d0e469594e","src/render/ui_vertex_output.wgsl":"d96c4b778ae25aa6674243655e48f37cdec5388a35f46fa7f2faec910bde1b60","src/stack.rs":"e0660924ee8f81729d2315364319c99463e55e32d13091c549a9bdc80bbdfd98","src/ui_material.rs":"c2e06bd135a50f6b59cca175a1d6373b0e6b299506e6af7abe2cedfd731cc9d8","src/ui_node.rs":"dbc62c473ae3020f36cb95f9562008f756d8afcea65e64f99c7b40bcd4d7905d","src/update.rs":"f9334ad2409dd7cac8622712069f6f43fe91f437c4c487b04d78d0d5fd4d2f6e","src/widget/button.rs":"ec2f740f6523274e40f81178893fdd932b081cc9f79185b536efc3f23b2bc09a","src/widget/image.rs":"8e31752381c370ad8b749b2ec60dc122036f76d4f7f0d64269125d8477a22a28","src/widget/label.rs":"2c9e771acee5d6cb3c2030ba7efc2f6b0d2e3c48128a4698fe7fe1187ee5f77c","src/widget/mod.rs":"e9c7c026433e2bb1c54549efdb210d3614e31fb68a474df675e60ea618e59202","src/widget/text.rs":"3ea72128efaaf4339130864e151d200dde75549d637178e5626080c2a9153e42"},"package":"ea4a4d2ba51865bc3039af29a26b4f52c48b54cc758369f52004caf4b6f03770"}

3501
vendor/bevy_ui/Cargo.lock generated vendored Normal file

File diff suppressed because it is too large Load Diff

182
vendor/bevy_ui/Cargo.toml vendored Normal file
View File

@@ -0,0 +1,182 @@
# THIS FILE IS AUTOMATICALLY GENERATED BY CARGO
#
# When uploading crates to the registry Cargo will automatically
# "normalize" Cargo.toml files for maximal compatibility
# with all versions of Cargo and also rewrite `path` dependencies
# to registry (e.g., crates.io) dependencies.
#
# If you are reading this file be aware that the original Cargo.toml
# will likely look very different (and much more reasonable).
# See Cargo.toml.orig for the original contents.
[package]
edition = "2024"
name = "bevy_ui"
version = "0.16.1"
build = false
autolib = false
autobins = false
autoexamples = false
autotests = false
autobenches = false
description = "A custom ECS-driven UI framework built specifically for Bevy Engine"
homepage = "https://bevyengine.org"
readme = "README.md"
keywords = ["bevy"]
license = "MIT OR Apache-2.0"
repository = "https://github.com/bevyengine/bevy"
resolver = "2"
[package.metadata.docs.rs]
all-features = true
rustdoc-args = [
"-Zunstable-options",
"--generate-link-to-definition",
]
[features]
bevy_ui_debug = []
bevy_ui_picking_backend = ["bevy_picking"]
default = []
ghost_nodes = []
serialize = [
"serde",
"smallvec/serde",
"bevy_math/serialize",
"bevy_platform/serialize",
]
[lib]
name = "bevy_ui"
path = "src/lib.rs"
[dependencies.accesskit]
version = "0.18"
[dependencies.bevy_a11y]
version = "0.16.1"
[dependencies.bevy_app]
version = "0.16.1"
[dependencies.bevy_asset]
version = "0.16.1"
[dependencies.bevy_color]
version = "0.16.2"
[dependencies.bevy_core_pipeline]
version = "0.16.1"
[dependencies.bevy_derive]
version = "0.16.1"
[dependencies.bevy_ecs]
version = "0.16.1"
[dependencies.bevy_image]
version = "0.16.1"
[dependencies.bevy_input]
version = "0.16.1"
[dependencies.bevy_math]
version = "0.16.1"
[dependencies.bevy_picking]
version = "0.16.1"
optional = true
[dependencies.bevy_platform]
version = "0.16.1"
features = ["std"]
default-features = false
[dependencies.bevy_reflect]
version = "0.16.1"
[dependencies.bevy_render]
version = "0.16.1"
[dependencies.bevy_sprite]
version = "0.16.1"
[dependencies.bevy_text]
version = "0.16.1"
[dependencies.bevy_transform]
version = "0.16.1"
[dependencies.bevy_utils]
version = "0.16.1"
[dependencies.bevy_window]
version = "0.16.1"
[dependencies.bytemuck]
version = "1.5"
features = ["derive"]
[dependencies.derive_more]
version = "1"
features = ["from"]
default-features = false
[dependencies.nonmax]
version = "0.5"
[dependencies.serde]
version = "1"
features = ["derive"]
optional = true
[dependencies.smallvec]
version = "1.11"
[dependencies.taffy]
version = "0.7"
[dependencies.thiserror]
version = "2"
default-features = false
[dependencies.tracing]
version = "0.1"
features = ["std"]
default-features = false
[lints.clippy]
alloc_instead_of_core = "warn"
allow_attributes = "warn"
allow_attributes_without_reason = "warn"
doc_markdown = "warn"
manual_let_else = "warn"
match_same_arms = "warn"
needless_lifetimes = "allow"
nonstandard_macro_braces = "warn"
print_stderr = "warn"
print_stdout = "warn"
ptr_as_ptr = "warn"
ptr_cast_constness = "warn"
redundant_closure_for_method_calls = "warn"
redundant_else = "warn"
ref_as_ptr = "warn"
semicolon_if_nothing_returned = "warn"
std_instead_of_alloc = "warn"
std_instead_of_core = "warn"
too_long_first_doc_paragraph = "allow"
too_many_arguments = "allow"
type_complexity = "allow"
undocumented_unsafe_blocks = "warn"
unwrap_or_default = "warn"
[lints.rust]
missing_docs = "warn"
unsafe_code = "deny"
unsafe_op_in_unsafe_fn = "warn"
unused_qualifications = "warn"
[lints.rust.unexpected_cfgs]
level = "warn"
priority = 0
check-cfg = ["cfg(docsrs_dep)"]

176
vendor/bevy_ui/LICENSE-APACHE vendored Normal file
View File

@@ -0,0 +1,176 @@
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS

19
vendor/bevy_ui/LICENSE-MIT vendored Normal file
View File

@@ -0,0 +1,19 @@
MIT License
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

7
vendor/bevy_ui/README.md vendored Normal file
View File

@@ -0,0 +1,7 @@
# Bevy UI
[![License](https://img.shields.io/badge/license-MIT%2FApache-blue.svg)](https://github.com/bevyengine/bevy#license)
[![Crates.io](https://img.shields.io/crates/v/bevy_ui.svg)](https://crates.io/crates/bevy_ui)
[![Downloads](https://img.shields.io/crates/d/bevy_ui.svg)](https://crates.io/crates/bevy_ui)
[![Docs](https://docs.rs/bevy_ui/badge.svg)](https://docs.rs/bevy_ui/latest/bevy_ui/)
[![Discord](https://img.shields.io/discord/691052431525675048.svg?label=&logo=discord&logoColor=ffffff&color=7389D8&labelColor=6A7EC2)](https://discord.gg/bevy)

164
vendor/bevy_ui/src/accessibility.rs vendored Normal file
View File

@@ -0,0 +1,164 @@
use crate::{
experimental::UiChildren,
prelude::{Button, Label},
widget::{ImageNode, TextUiReader},
ComputedNode,
};
use bevy_a11y::AccessibilityNode;
use bevy_app::{App, Plugin, PostUpdate};
use bevy_ecs::{
prelude::{DetectChanges, Entity},
query::{Changed, Without},
schedule::IntoScheduleConfigs,
system::{Commands, Query},
world::Ref,
};
use bevy_math::Vec3Swizzles;
use bevy_render::camera::CameraUpdateSystem;
use bevy_transform::prelude::GlobalTransform;
use accesskit::{Node, Rect, Role};
fn calc_label(
text_reader: &mut TextUiReader,
children: impl Iterator<Item = Entity>,
) -> Option<Box<str>> {
let mut name = None;
for child in children {
let values = text_reader
.iter(child)
.map(|(_, _, text, _, _)| text.into())
.collect::<Vec<String>>();
if !values.is_empty() {
name = Some(values.join(" "));
}
}
name.map(String::into_boxed_str)
}
fn calc_bounds(
mut nodes: Query<(
&mut AccessibilityNode,
Ref<ComputedNode>,
Ref<GlobalTransform>,
)>,
) {
for (mut accessible, node, transform) in &mut nodes {
if node.is_changed() || transform.is_changed() {
let center = transform.translation().xy();
let half_size = 0.5 * node.size;
let min = center - half_size;
let max = center + half_size;
let bounds = Rect::new(min.x as f64, min.y as f64, max.x as f64, max.y as f64);
accessible.set_bounds(bounds);
}
}
}
fn button_changed(
mut commands: Commands,
mut query: Query<(Entity, Option<&mut AccessibilityNode>), Changed<Button>>,
ui_children: UiChildren,
mut text_reader: TextUiReader,
) {
for (entity, accessible) in &mut query {
let label = calc_label(&mut text_reader, ui_children.iter_ui_children(entity));
if let Some(mut accessible) = accessible {
accessible.set_role(Role::Button);
if let Some(name) = label {
accessible.set_label(name);
} else {
accessible.clear_label();
}
} else {
let mut node = Node::new(Role::Button);
if let Some(label) = label {
node.set_label(label);
}
commands
.entity(entity)
.try_insert(AccessibilityNode::from(node));
}
}
}
fn image_changed(
mut commands: Commands,
mut query: Query<
(Entity, Option<&mut AccessibilityNode>),
(Changed<ImageNode>, Without<Button>),
>,
ui_children: UiChildren,
mut text_reader: TextUiReader,
) {
for (entity, accessible) in &mut query {
let label = calc_label(&mut text_reader, ui_children.iter_ui_children(entity));
if let Some(mut accessible) = accessible {
accessible.set_role(Role::Image);
if let Some(label) = label {
accessible.set_label(label);
} else {
accessible.clear_label();
}
} else {
let mut node = Node::new(Role::Image);
if let Some(label) = label {
node.set_label(label);
}
commands
.entity(entity)
.try_insert(AccessibilityNode::from(node));
}
}
}
fn label_changed(
mut commands: Commands,
mut query: Query<(Entity, Option<&mut AccessibilityNode>), Changed<Label>>,
mut text_reader: TextUiReader,
) {
for (entity, accessible) in &mut query {
let values = text_reader
.iter(entity)
.map(|(_, _, text, _, _)| text.into())
.collect::<Vec<String>>();
let label = Some(values.join(" ").into_boxed_str());
if let Some(mut accessible) = accessible {
accessible.set_role(Role::Label);
if let Some(label) = label {
accessible.set_value(label);
} else {
accessible.clear_value();
}
} else {
let mut node = Node::new(Role::Label);
if let Some(label) = label {
node.set_value(label);
}
commands
.entity(entity)
.try_insert(AccessibilityNode::from(node));
}
}
}
/// `AccessKit` integration for `bevy_ui`.
pub(crate) struct AccessibilityPlugin;
impl Plugin for AccessibilityPlugin {
fn build(&self, app: &mut App) {
app.add_systems(
PostUpdate,
(
calc_bounds
.after(bevy_transform::TransformSystem::TransformPropagate)
.after(CameraUpdateSystem)
// the listed systems do not affect calculated size
.ambiguous_with(crate::ui_stack_system),
button_changed,
image_changed,
label_changed,
),
);
}
}

View File

@@ -0,0 +1,274 @@
//! This module contains [`GhostNode`] and utilities to flatten the UI hierarchy, traversing past ghost nodes.
#[cfg(feature = "ghost_nodes")]
use crate::ui_node::ComputedNodeTarget;
use crate::Node;
use bevy_ecs::{prelude::*, system::SystemParam};
#[cfg(feature = "ghost_nodes")]
use bevy_reflect::prelude::*;
#[cfg(feature = "ghost_nodes")]
use bevy_render::view::Visibility;
#[cfg(feature = "ghost_nodes")]
use bevy_transform::prelude::Transform;
#[cfg(feature = "ghost_nodes")]
use smallvec::SmallVec;
/// Marker component for entities that should be ignored within UI hierarchies.
///
/// The UI systems will traverse past these and treat their first non-ghost descendants as direct children of their first non-ghost ancestor.
///
/// Any components necessary for transform and visibility propagation will be added automatically.
#[cfg(feature = "ghost_nodes")]
#[derive(Component, Debug, Copy, Clone, Reflect)]
#[cfg_attr(feature = "ghost_nodes", derive(Default))]
#[reflect(Component, Debug, Clone)]
#[require(Visibility, Transform, ComputedNodeTarget)]
pub struct GhostNode;
#[cfg(feature = "ghost_nodes")]
/// System param that allows iteration of all UI root nodes.
///
/// A UI root node is either a [`Node`] without a [`ChildOf`], or with only [`GhostNode`] ancestors.
#[derive(SystemParam)]
pub struct UiRootNodes<'w, 's> {
root_node_query: Query<'w, 's, Entity, (With<Node>, Without<ChildOf>)>,
root_ghost_node_query: Query<'w, 's, Entity, (With<GhostNode>, Without<ChildOf>)>,
all_nodes_query: Query<'w, 's, Entity, With<Node>>,
ui_children: UiChildren<'w, 's>,
}
#[cfg(not(feature = "ghost_nodes"))]
pub type UiRootNodes<'w, 's> = Query<'w, 's, Entity, (With<Node>, Without<ChildOf>)>;
#[cfg(feature = "ghost_nodes")]
impl<'w, 's> UiRootNodes<'w, 's> {
pub fn iter(&'s self) -> impl Iterator<Item = Entity> + 's {
self.root_node_query
.iter()
.chain(self.root_ghost_node_query.iter().flat_map(|root_ghost| {
self.all_nodes_query
.iter_many(self.ui_children.iter_ui_children(root_ghost))
}))
}
}
#[cfg(feature = "ghost_nodes")]
/// System param that gives access to UI children utilities, skipping over [`GhostNode`].
#[derive(SystemParam)]
pub struct UiChildren<'w, 's> {
ui_children_query: Query<
'w,
's,
(Option<&'static Children>, Has<GhostNode>),
Or<(With<Node>, With<GhostNode>)>,
>,
changed_children_query: Query<'w, 's, Entity, Changed<Children>>,
children_query: Query<'w, 's, &'static Children>,
ghost_nodes_query: Query<'w, 's, Entity, With<GhostNode>>,
parents_query: Query<'w, 's, &'static ChildOf>,
}
#[cfg(not(feature = "ghost_nodes"))]
/// System param that gives access to UI children utilities.
#[derive(SystemParam)]
pub struct UiChildren<'w, 's> {
ui_children_query: Query<'w, 's, Option<&'static Children>, With<Node>>,
changed_children_query: Query<'w, 's, Entity, Changed<Children>>,
parents_query: Query<'w, 's, &'static ChildOf>,
}
#[cfg(feature = "ghost_nodes")]
impl<'w, 's> UiChildren<'w, 's> {
/// Iterates the children of `entity`, skipping over [`GhostNode`].
///
/// Traverses the hierarchy depth-first to ensure child order.
///
/// # Performance
///
/// This iterator allocates if the `entity` node has more than 8 children (including ghost nodes).
pub fn iter_ui_children(&'s self, entity: Entity) -> UiChildrenIter<'w, 's> {
UiChildrenIter {
stack: self
.ui_children_query
.get(entity)
.map_or(SmallVec::new(), |(children, _)| {
children.into_iter().flatten().rev().copied().collect()
}),
query: &self.ui_children_query,
}
}
/// Returns the UI parent of the provided entity, skipping over [`GhostNode`].
pub fn get_parent(&'s self, entity: Entity) -> Option<Entity> {
self.parents_query
.iter_ancestors(entity)
.find(|entity| !self.ghost_nodes_query.contains(*entity))
}
/// Iterates the [`GhostNode`]s between this entity and its UI children.
pub fn iter_ghost_nodes(&'s self, entity: Entity) -> Box<dyn Iterator<Item = Entity> + 's> {
Box::new(
self.children_query
.get(entity)
.into_iter()
.flat_map(|children| {
self.ghost_nodes_query
.iter_many(children)
.flat_map(|entity| {
core::iter::once(entity).chain(self.iter_ghost_nodes(entity))
})
}),
)
}
/// Given an entity in the UI hierarchy, check if its set of children has changed, e.g if children has been added/removed or if the order has changed.
pub fn is_changed(&'s self, entity: Entity) -> bool {
self.changed_children_query.contains(entity)
|| self
.iter_ghost_nodes(entity)
.any(|entity| self.changed_children_query.contains(entity))
}
/// Returns `true` if the given entity is either a [`Node`] or a [`GhostNode`].
pub fn is_ui_node(&'s self, entity: Entity) -> bool {
self.ui_children_query.contains(entity)
}
}
#[cfg(not(feature = "ghost_nodes"))]
impl<'w, 's> UiChildren<'w, 's> {
/// Iterates the children of `entity`.
pub fn iter_ui_children(&'s self, entity: Entity) -> impl Iterator<Item = Entity> + 's {
self.ui_children_query
.get(entity)
.ok()
.flatten()
.map(|children| children.as_ref())
.unwrap_or(&[])
.iter()
.copied()
}
/// Returns the UI parent of the provided entity.
pub fn get_parent(&'s self, entity: Entity) -> Option<Entity> {
self.parents_query.get(entity).ok().map(ChildOf::parent)
}
/// Given an entity in the UI hierarchy, check if its set of children has changed, e.g if children has been added/removed or if the order has changed.
pub fn is_changed(&'s self, entity: Entity) -> bool {
self.changed_children_query.contains(entity)
}
/// Returns `true` if the given entity is either a [`Node`] or a [`GhostNode`].
pub fn is_ui_node(&'s self, entity: Entity) -> bool {
self.ui_children_query.contains(entity)
}
}
#[cfg(feature = "ghost_nodes")]
pub struct UiChildrenIter<'w, 's> {
stack: SmallVec<[Entity; 8]>,
query: &'s Query<
'w,
's,
(Option<&'static Children>, Has<GhostNode>),
Or<(With<Node>, With<GhostNode>)>,
>,
}
#[cfg(feature = "ghost_nodes")]
impl<'w, 's> Iterator for UiChildrenIter<'w, 's> {
type Item = Entity;
fn next(&mut self) -> Option<Self::Item> {
loop {
let entity = self.stack.pop()?;
if let Ok((children, has_ghost_node)) = self.query.get(entity) {
if !has_ghost_node {
return Some(entity);
}
if let Some(children) = children {
self.stack.extend(children.iter().rev());
}
}
}
}
}
#[cfg(all(test, feature = "ghost_nodes"))]
mod tests {
use bevy_ecs::{
prelude::Component,
system::{Query, SystemState},
world::World,
};
use super::{GhostNode, Node, UiChildren, UiRootNodes};
#[derive(Component, PartialEq, Debug)]
struct A(usize);
#[test]
fn iterate_ui_root_nodes() {
let world = &mut World::new();
// Normal root
world
.spawn((A(1), Node::default()))
.with_children(|parent| {
parent.spawn((A(2), Node::default()));
parent
.spawn((A(3), GhostNode))
.with_child((A(4), Node::default()));
});
// Ghost root
world.spawn((A(5), GhostNode)).with_children(|parent| {
parent.spawn((A(6), Node::default()));
parent
.spawn((A(7), GhostNode))
.with_child((A(8), Node::default()))
.with_child(A(9));
});
let mut system_state = SystemState::<(UiRootNodes, Query<&A>)>::new(world);
let (ui_root_nodes, a_query) = system_state.get(world);
let result: Vec<_> = a_query.iter_many(ui_root_nodes.iter()).collect();
assert_eq!([&A(1), &A(6), &A(8)], result.as_slice());
}
#[test]
fn iterate_ui_children() {
let world = &mut World::new();
let n1 = world.spawn((A(1), Node::default())).id();
let n2 = world.spawn((A(2), GhostNode)).id();
let n3 = world.spawn((A(3), GhostNode)).id();
let n4 = world.spawn((A(4), Node::default())).id();
let n5 = world.spawn((A(5), Node::default())).id();
let n6 = world.spawn((A(6), GhostNode)).id();
let n7 = world.spawn((A(7), GhostNode)).id();
let n8 = world.spawn((A(8), Node::default())).id();
let n9 = world.spawn((A(9), GhostNode)).id();
let n10 = world.spawn((A(10), Node::default())).id();
let no_ui = world.spawn_empty().id();
world.entity_mut(n1).add_children(&[n2, n3, n4, n6]);
world.entity_mut(n2).add_children(&[n5]);
world.entity_mut(n6).add_children(&[n7, no_ui, n9]);
world.entity_mut(n7).add_children(&[n8]);
world.entity_mut(n9).add_children(&[n10]);
let mut system_state = SystemState::<(UiChildren, Query<&A>)>::new(world);
let (ui_children, a_query) = system_state.get(world);
let result: Vec<_> = a_query
.iter_many(ui_children.iter_ui_children(n1))
.collect();
assert_eq!([&A(5), &A(4), &A(8), &A(10)], result.as_slice());
}
}

17
vendor/bevy_ui/src/experimental/mod.rs vendored Normal file
View File

@@ -0,0 +1,17 @@
//! Experimental features are not yet stable and may change or be removed in the future.
//!
//! These features are not recommended for production use, but are available to ease experimentation
//! within Bevy's ecosystem. Please let us know how you are using these features and what you would
//! like to see improved!
//!
//! These may be feature-flagged: check the `Cargo.toml` for `bevy_ui` to see what options
//! are available.
//!
//! # Warning
//!
//! Be careful when using these features, especially in concert with third-party crates,
//! as they may not be fully supported, functional or stable.
mod ghost_hierarchy;
pub use ghost_hierarchy::*;

359
vendor/bevy_ui/src/focus.rs vendored Normal file
View File

@@ -0,0 +1,359 @@
use crate::{CalculatedClip, ComputedNode, ComputedNodeTarget, ResolvedBorderRadius, UiStack};
use bevy_ecs::{
change_detection::DetectChangesMut,
entity::{ContainsEntity, Entity},
prelude::{Component, With},
query::QueryData,
reflect::ReflectComponent,
system::{Local, Query, Res},
};
use bevy_input::{mouse::MouseButton, touch::Touches, ButtonInput};
use bevy_math::{Rect, Vec2};
use bevy_platform::collections::HashMap;
use bevy_reflect::{std_traits::ReflectDefault, Reflect};
use bevy_render::{camera::NormalizedRenderTarget, prelude::Camera, view::InheritedVisibility};
use bevy_transform::components::GlobalTransform;
use bevy_window::{PrimaryWindow, Window};
use smallvec::SmallVec;
#[cfg(feature = "serialize")]
use bevy_reflect::{ReflectDeserialize, ReflectSerialize};
/// Describes what type of input interaction has occurred for a UI node.
///
/// This is commonly queried with a `Changed<Interaction>` filter.
///
/// Updated in [`ui_focus_system`].
///
/// If a UI node has both [`Interaction`] and [`InheritedVisibility`] components,
/// [`Interaction`] will always be [`Interaction::None`]
/// when [`InheritedVisibility::get()`] is false.
/// This ensures that hidden UI nodes are not interactable,
/// and do not end up stuck in an active state if hidden at the wrong time.
///
/// Note that you can also control the visibility of a node using the [`Display`](crate::ui_node::Display) property,
/// which fully collapses it during layout calculations.
///
/// # See also
///
/// - [`Button`](crate::widget::Button) which requires this component
/// - [`RelativeCursorPosition`] to obtain the position of the cursor relative to current node
#[derive(Component, Copy, Clone, Eq, PartialEq, Debug, Reflect)]
#[reflect(Component, Default, PartialEq, Debug, Clone)]
#[cfg_attr(
feature = "serialize",
derive(serde::Serialize, serde::Deserialize),
reflect(Serialize, Deserialize)
)]
pub enum Interaction {
/// The node has been pressed.
///
/// Note: This does not capture click/press-release action.
Pressed,
/// The node has been hovered over
Hovered,
/// Nothing has happened
None,
}
impl Interaction {
const DEFAULT: Self = Self::None;
}
impl Default for Interaction {
fn default() -> Self {
Self::DEFAULT
}
}
/// A component storing the position of the mouse relative to the node, (0., 0.) being the top-left corner and (1., 1.) being the bottom-right
/// If the mouse is not over the node, the value will go beyond the range of (0., 0.) to (1., 1.)
///
/// It can be used alongside [`Interaction`] to get the position of the press.
///
/// The component is updated when it is in the same entity with [`Node`](crate::Node).
#[derive(Component, Copy, Clone, Default, PartialEq, Debug, Reflect)]
#[reflect(Component, Default, PartialEq, Debug, Clone)]
#[cfg_attr(
feature = "serialize",
derive(serde::Serialize, serde::Deserialize),
reflect(Serialize, Deserialize)
)]
pub struct RelativeCursorPosition {
/// Visible area of the Node relative to the size of the entire Node.
pub normalized_visible_node_rect: Rect,
/// Cursor position relative to the size and position of the Node.
/// A None value indicates that the cursor position is unknown.
pub normalized: Option<Vec2>,
}
impl RelativeCursorPosition {
/// A helper function to check if the mouse is over the node
pub fn mouse_over(&self) -> bool {
self.normalized
.is_some_and(|position| self.normalized_visible_node_rect.contains(position))
}
}
/// Describes whether the node should block interactions with lower nodes
#[derive(Component, Copy, Clone, Eq, PartialEq, Debug, Reflect)]
#[reflect(Component, Default, PartialEq, Debug, Clone)]
#[cfg_attr(
feature = "serialize",
derive(serde::Serialize, serde::Deserialize),
reflect(Serialize, Deserialize)
)]
pub enum FocusPolicy {
/// Blocks interaction
Block,
/// Lets interaction pass through
Pass,
}
impl FocusPolicy {
const DEFAULT: Self = Self::Pass;
}
impl Default for FocusPolicy {
fn default() -> Self {
Self::DEFAULT
}
}
/// Contains entities whose Interaction should be set to None
#[derive(Default)]
pub struct State {
entities_to_reset: SmallVec<[Entity; 1]>,
}
/// Main query for [`ui_focus_system`]
#[derive(QueryData)]
#[query_data(mutable)]
pub struct NodeQuery {
entity: Entity,
node: &'static ComputedNode,
global_transform: &'static GlobalTransform,
interaction: Option<&'static mut Interaction>,
relative_cursor_position: Option<&'static mut RelativeCursorPosition>,
focus_policy: Option<&'static FocusPolicy>,
calculated_clip: Option<&'static CalculatedClip>,
inherited_visibility: Option<&'static InheritedVisibility>,
target_camera: &'static ComputedNodeTarget,
}
/// The system that sets Interaction for all UI elements based on the mouse cursor activity
///
/// Entities with a hidden [`InheritedVisibility`] are always treated as released.
pub fn ui_focus_system(
mut state: Local<State>,
camera_query: Query<(Entity, &Camera)>,
primary_window: Query<Entity, With<PrimaryWindow>>,
windows: Query<&Window>,
mouse_button_input: Res<ButtonInput<MouseButton>>,
touches_input: Res<Touches>,
ui_stack: Res<UiStack>,
mut node_query: Query<NodeQuery>,
) {
let primary_window = primary_window.iter().next();
// reset entities that were both clicked and released in the last frame
for entity in state.entities_to_reset.drain(..) {
if let Ok(NodeQueryItem {
interaction: Some(mut interaction),
..
}) = node_query.get_mut(entity)
{
*interaction = Interaction::None;
}
}
let mouse_released =
mouse_button_input.just_released(MouseButton::Left) || touches_input.any_just_released();
if mouse_released {
for node in &mut node_query {
if let Some(mut interaction) = node.interaction {
if *interaction == Interaction::Pressed {
*interaction = Interaction::None;
}
}
}
}
let mouse_clicked =
mouse_button_input.just_pressed(MouseButton::Left) || touches_input.any_just_pressed();
let camera_cursor_positions: HashMap<Entity, Vec2> = camera_query
.iter()
.filter_map(|(entity, camera)| {
// Interactions are only supported for cameras rendering to a window.
let Some(NormalizedRenderTarget::Window(window_ref)) =
camera.target.normalize(primary_window)
else {
return None;
};
let window = windows.get(window_ref.entity()).ok()?;
let viewport_position = camera
.physical_viewport_rect()
.map(|rect| rect.min.as_vec2())
.unwrap_or_default();
window
.physical_cursor_position()
.or_else(|| {
touches_input
.first_pressed_position()
.map(|pos| pos * window.scale_factor())
})
.map(|cursor_position| (entity, cursor_position - viewport_position))
})
.collect();
// prepare an iterator that contains all the nodes that have the cursor in their rect,
// from the top node to the bottom one. this will also reset the interaction to `None`
// for all nodes encountered that are no longer hovered.
let mut hovered_nodes = ui_stack
.uinodes
.iter()
// reverse the iterator to traverse the tree from closest nodes to furthest
.rev()
.filter_map(|entity| {
let Ok(node) = node_query.get_mut(*entity) else {
return None;
};
let inherited_visibility = node.inherited_visibility?;
// Nodes that are not rendered should not be interactable
if !inherited_visibility.get() {
// Reset their interaction to None to avoid strange stuck state
if let Some(mut interaction) = node.interaction {
// We cannot simply set the interaction to None, as that will trigger change detection repeatedly
interaction.set_if_neq(Interaction::None);
}
return None;
}
let camera_entity = node.target_camera.camera()?;
let node_rect = Rect::from_center_size(
node.global_transform.translation().truncate(),
node.node.size(),
);
// Intersect with the calculated clip rect to find the bounds of the visible region of the node
let visible_rect = node
.calculated_clip
.map(|clip| node_rect.intersect(clip.clip))
.unwrap_or(node_rect);
let cursor_position = camera_cursor_positions.get(&camera_entity);
// The mouse position relative to the node
// (0., 0.) is the top-left corner, (1., 1.) is the bottom-right corner
// Coordinates are relative to the entire node, not just the visible region.
let relative_cursor_position = cursor_position.and_then(|cursor_position| {
// ensure node size is non-zero in all dimensions, otherwise relative position will be
// +/-inf. if the node is hidden, the visible rect min/max will also be -inf leading to
// false positives for mouse_over (#12395)
(node_rect.size().cmpgt(Vec2::ZERO).all())
.then_some((*cursor_position - node_rect.min) / node_rect.size())
});
// If the current cursor position is within the bounds of the node's visible area, consider it for
// clicking
let relative_cursor_position_component = RelativeCursorPosition {
normalized_visible_node_rect: visible_rect.normalize(node_rect),
normalized: relative_cursor_position,
};
let contains_cursor = relative_cursor_position_component.mouse_over()
&& cursor_position.is_some_and(|point| {
pick_rounded_rect(
*point - node_rect.center(),
node_rect.size(),
node.node.border_radius,
)
});
// Save the relative cursor position to the correct component
if let Some(mut node_relative_cursor_position_component) = node.relative_cursor_position
{
*node_relative_cursor_position_component = relative_cursor_position_component;
}
if contains_cursor {
Some(*entity)
} else {
if let Some(mut interaction) = node.interaction {
if *interaction == Interaction::Hovered || (relative_cursor_position.is_none())
{
interaction.set_if_neq(Interaction::None);
}
}
None
}
})
.collect::<Vec<Entity>>()
.into_iter();
// set Pressed or Hovered on top nodes. as soon as a node with a `Block` focus policy is detected,
// the iteration will stop on it because it "captures" the interaction.
let mut iter = node_query.iter_many_mut(hovered_nodes.by_ref());
while let Some(node) = iter.fetch_next() {
if let Some(mut interaction) = node.interaction {
if mouse_clicked {
// only consider nodes with Interaction "pressed"
if *interaction != Interaction::Pressed {
*interaction = Interaction::Pressed;
// if the mouse was simultaneously released, reset this Interaction in the next
// frame
if mouse_released {
state.entities_to_reset.push(node.entity);
}
}
} else if *interaction == Interaction::None {
*interaction = Interaction::Hovered;
}
}
match node.focus_policy.unwrap_or(&FocusPolicy::Block) {
FocusPolicy::Block => {
break;
}
FocusPolicy::Pass => { /* allow the next node to be hovered/pressed */ }
}
}
// reset `Interaction` for the remaining lower nodes to `None`. those are the nodes that remain in
// `moused_over_nodes` after the previous loop is exited.
let mut iter = node_query.iter_many_mut(hovered_nodes);
while let Some(node) = iter.fetch_next() {
if let Some(mut interaction) = node.interaction {
// don't reset pressed nodes because they're handled separately
if *interaction != Interaction::Pressed {
interaction.set_if_neq(Interaction::None);
}
}
}
}
// Returns true if `point` (relative to the rectangle's center) is within the bounds of a rounded rectangle with
// the given size and border radius.
//
// Matches the sdf function in `ui.wgsl` that is used by the UI renderer to draw rounded rectangles.
pub(crate) fn pick_rounded_rect(
point: Vec2,
size: Vec2,
border_radius: ResolvedBorderRadius,
) -> bool {
let [top, bottom] = if point.x < 0. {
[border_radius.top_left, border_radius.bottom_left]
} else {
[border_radius.top_right, border_radius.bottom_right]
};
let r = if point.y < 0. { top } else { bottom };
let corner_to_point = point.abs() - 0.5 * size;
let q = corner_to_point + r;
let l = q.max(Vec2::ZERO).length();
let m = q.max_element().min(0.);
l + m - r < 0.
}

847
vendor/bevy_ui/src/geometry.rs vendored Normal file
View File

@@ -0,0 +1,847 @@
use bevy_math::Vec2;
use bevy_reflect::{std_traits::ReflectDefault, Reflect};
use core::ops::{Div, DivAssign, Mul, MulAssign, Neg};
use thiserror::Error;
#[cfg(feature = "serialize")]
use bevy_reflect::{ReflectDeserialize, ReflectSerialize};
/// Represents the possible value types for layout properties.
///
/// This enum allows specifying values for various [`Node`](crate::Node) properties in different units,
/// such as logical pixels, percentages, or automatically determined values.
///
/// `Val` also implements [`core::str::FromStr`] to allow parsing values from strings in the format `#.#px`. Whitespaces between the value and unit is allowed. The following units are supported:
/// * `px`: logical pixels
/// * `%`: percentage
/// * `vw`: percentage of the viewport width
/// * `vh`: percentage of the viewport height
/// * `vmin`: percentage of the viewport's smaller dimension
/// * `vmax`: percentage of the viewport's larger dimension
///
/// Additionally, `auto` will be parsed as [`Val::Auto`].
#[derive(Copy, Clone, Debug, Reflect)]
#[reflect(Default, PartialEq, Debug, Clone)]
#[cfg_attr(
feature = "serialize",
derive(serde::Serialize, serde::Deserialize),
reflect(Serialize, Deserialize)
)]
pub enum Val {
/// Automatically determine the value based on the context and other [`Node`](crate::Node) properties.
Auto,
/// Set this value in logical pixels.
Px(f32),
/// Set the value as a percentage of its parent node's length along a specific axis.
///
/// If the UI node has no parent, the percentage is calculated based on the window's length
/// along the corresponding axis.
///
/// The chosen axis depends on the [`Node`](crate::Node) field set:
/// * For `flex_basis`, the percentage is relative to the main-axis length determined by the `flex_direction`.
/// * For `gap`, `min_size`, `size`, and `max_size`:
/// - `width` is relative to the parent's width.
/// - `height` is relative to the parent's height.
/// * For `margin`, `padding`, and `border` values: the percentage is relative to the parent node's width.
/// * For positions, `left` and `right` are relative to the parent's width, while `bottom` and `top` are relative to the parent's height.
Percent(f32),
/// Set this value in percent of the viewport width
Vw(f32),
/// Set this value in percent of the viewport height
Vh(f32),
/// Set this value in percent of the viewport's smaller dimension.
VMin(f32),
/// Set this value in percent of the viewport's larger dimension.
VMax(f32),
}
#[derive(Debug, Error, PartialEq, Eq)]
pub enum ValParseError {
UnitMissing,
ValueMissing,
InvalidValue,
InvalidUnit,
}
impl core::fmt::Display for ValParseError {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
match self {
ValParseError::UnitMissing => write!(f, "unit missing"),
ValParseError::ValueMissing => write!(f, "value missing"),
ValParseError::InvalidValue => write!(f, "invalid value"),
ValParseError::InvalidUnit => write!(f, "invalid unit"),
}
}
}
impl core::str::FromStr for Val {
type Err = ValParseError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
let s = s.trim();
if s.eq_ignore_ascii_case("auto") {
return Ok(Val::Auto);
}
let Some(end_of_number) = s
.bytes()
.position(|c| !(c.is_ascii_digit() || c == b'.' || c == b'-' || c == b'+'))
else {
return Err(ValParseError::UnitMissing);
};
if end_of_number == 0 {
return Err(ValParseError::ValueMissing);
}
let (value, unit) = s.split_at(end_of_number);
let value: f32 = value.parse().map_err(|_| ValParseError::InvalidValue)?;
let unit = unit.trim();
if unit.eq_ignore_ascii_case("px") {
Ok(Val::Px(value))
} else if unit.eq_ignore_ascii_case("%") {
Ok(Val::Percent(value))
} else if unit.eq_ignore_ascii_case("vw") {
Ok(Val::Vw(value))
} else if unit.eq_ignore_ascii_case("vh") {
Ok(Val::Vh(value))
} else if unit.eq_ignore_ascii_case("vmin") {
Ok(Val::VMin(value))
} else if unit.eq_ignore_ascii_case("vmax") {
Ok(Val::VMax(value))
} else {
Err(ValParseError::InvalidUnit)
}
}
}
impl PartialEq for Val {
fn eq(&self, other: &Self) -> bool {
let same_unit = matches!(
(self, other),
(Self::Auto, Self::Auto)
| (Self::Px(_), Self::Px(_))
| (Self::Percent(_), Self::Percent(_))
| (Self::Vw(_), Self::Vw(_))
| (Self::Vh(_), Self::Vh(_))
| (Self::VMin(_), Self::VMin(_))
| (Self::VMax(_), Self::VMax(_))
);
let left = match self {
Self::Auto => None,
Self::Px(v)
| Self::Percent(v)
| Self::Vw(v)
| Self::Vh(v)
| Self::VMin(v)
| Self::VMax(v) => Some(v),
};
let right = match other {
Self::Auto => None,
Self::Px(v)
| Self::Percent(v)
| Self::Vw(v)
| Self::Vh(v)
| Self::VMin(v)
| Self::VMax(v) => Some(v),
};
match (same_unit, left, right) {
(true, a, b) => a == b,
// All zero-value variants are considered equal.
(false, Some(&a), Some(&b)) => a == 0. && b == 0.,
_ => false,
}
}
}
impl Val {
pub const DEFAULT: Self = Self::Auto;
pub const ZERO: Self = Self::Px(0.0);
}
impl Default for Val {
fn default() -> Self {
Self::DEFAULT
}
}
impl Mul<f32> for Val {
type Output = Val;
fn mul(self, rhs: f32) -> Self::Output {
match self {
Val::Auto => Val::Auto,
Val::Px(value) => Val::Px(value * rhs),
Val::Percent(value) => Val::Percent(value * rhs),
Val::Vw(value) => Val::Vw(value * rhs),
Val::Vh(value) => Val::Vh(value * rhs),
Val::VMin(value) => Val::VMin(value * rhs),
Val::VMax(value) => Val::VMax(value * rhs),
}
}
}
impl MulAssign<f32> for Val {
fn mul_assign(&mut self, rhs: f32) {
match self {
Val::Auto => {}
Val::Px(value)
| Val::Percent(value)
| Val::Vw(value)
| Val::Vh(value)
| Val::VMin(value)
| Val::VMax(value) => *value *= rhs,
}
}
}
impl Div<f32> for Val {
type Output = Val;
fn div(self, rhs: f32) -> Self::Output {
match self {
Val::Auto => Val::Auto,
Val::Px(value) => Val::Px(value / rhs),
Val::Percent(value) => Val::Percent(value / rhs),
Val::Vw(value) => Val::Vw(value / rhs),
Val::Vh(value) => Val::Vh(value / rhs),
Val::VMin(value) => Val::VMin(value / rhs),
Val::VMax(value) => Val::VMax(value / rhs),
}
}
}
impl DivAssign<f32> for Val {
fn div_assign(&mut self, rhs: f32) {
match self {
Val::Auto => {}
Val::Px(value)
| Val::Percent(value)
| Val::Vw(value)
| Val::Vh(value)
| Val::VMin(value)
| Val::VMax(value) => *value /= rhs,
}
}
}
impl Neg for Val {
type Output = Val;
fn neg(self) -> Self::Output {
match self {
Val::Px(value) => Val::Px(-value),
Val::Percent(value) => Val::Percent(-value),
Val::Vw(value) => Val::Vw(-value),
Val::Vh(value) => Val::Vh(-value),
Val::VMin(value) => Val::VMin(-value),
Val::VMax(value) => Val::VMax(-value),
_ => self,
}
}
}
#[derive(Debug, Eq, PartialEq, Clone, Copy, Error)]
pub enum ValArithmeticError {
#[error("the given variant of Val is not evaluable (non-numeric)")]
NonEvaluable,
}
impl Val {
/// Resolves a [`Val`] from the given context values and returns this as an [`f32`].
/// The [`Val::Px`] value (if present), `parent_size` and `viewport_size` should all be in the same coordinate space.
/// Returns a [`ValArithmeticError::NonEvaluable`] if the [`Val`] is impossible to resolve into a concrete value.
///
/// **Note:** If a [`Val::Px`] is resolved, its inner value is returned unchanged.
pub fn resolve(self, parent_size: f32, viewport_size: Vec2) -> Result<f32, ValArithmeticError> {
match self {
Val::Percent(value) => Ok(parent_size * value / 100.0),
Val::Px(value) => Ok(value),
Val::Vw(value) => Ok(viewport_size.x * value / 100.0),
Val::Vh(value) => Ok(viewport_size.y * value / 100.0),
Val::VMin(value) => Ok(viewport_size.min_element() * value / 100.0),
Val::VMax(value) => Ok(viewport_size.max_element() * value / 100.0),
Val::Auto => Err(ValArithmeticError::NonEvaluable),
}
}
}
/// A type which is commonly used to define margins, paddings and borders.
///
/// # Examples
///
/// ## Margin
///
/// A margin is used to create space around UI elements, outside of any defined borders.
///
/// ```
/// # use bevy_ui::{UiRect, Val};
/// #
/// let margin = UiRect::all(Val::Auto); // Centers the UI element
/// ```
///
/// ## Padding
///
/// A padding is used to create space around UI elements, inside of any defined borders.
///
/// ```
/// # use bevy_ui::{UiRect, Val};
/// #
/// let padding = UiRect {
/// left: Val::Px(10.0),
/// right: Val::Px(20.0),
/// top: Val::Px(30.0),
/// bottom: Val::Px(40.0),
/// };
/// ```
///
/// ## Borders
///
/// A border is used to define the width of the border of a UI element.
///
/// ```
/// # use bevy_ui::{UiRect, Val};
/// #
/// let border = UiRect {
/// left: Val::Px(10.0),
/// right: Val::Px(20.0),
/// top: Val::Px(30.0),
/// bottom: Val::Px(40.0),
/// };
/// ```
#[derive(Copy, Clone, PartialEq, Debug, Reflect)]
#[reflect(Default, PartialEq, Debug, Clone)]
#[cfg_attr(
feature = "serialize",
derive(serde::Serialize, serde::Deserialize),
reflect(Serialize, Deserialize)
)]
pub struct UiRect {
/// The value corresponding to the left side of the UI rect.
pub left: Val,
/// The value corresponding to the right side of the UI rect.
pub right: Val,
/// The value corresponding to the top side of the UI rect.
pub top: Val,
/// The value corresponding to the bottom side of the UI rect.
pub bottom: Val,
}
impl UiRect {
pub const DEFAULT: Self = Self::all(Val::ZERO);
pub const ZERO: Self = Self::all(Val::ZERO);
pub const AUTO: Self = Self::all(Val::Auto);
/// Creates a new [`UiRect`] from the values specified.
///
/// # Example
///
/// ```
/// # use bevy_ui::{UiRect, Val};
/// #
/// let ui_rect = UiRect::new(
/// Val::Px(10.0),
/// Val::Px(20.0),
/// Val::Px(30.0),
/// Val::Px(40.0),
/// );
///
/// assert_eq!(ui_rect.left, Val::Px(10.0));
/// assert_eq!(ui_rect.right, Val::Px(20.0));
/// assert_eq!(ui_rect.top, Val::Px(30.0));
/// assert_eq!(ui_rect.bottom, Val::Px(40.0));
/// ```
pub const fn new(left: Val, right: Val, top: Val, bottom: Val) -> Self {
UiRect {
left,
right,
top,
bottom,
}
}
/// Creates a new [`UiRect`] where all sides have the same value.
///
/// # Example
///
/// ```
/// # use bevy_ui::{UiRect, Val};
/// #
/// let ui_rect = UiRect::all(Val::Px(10.0));
///
/// assert_eq!(ui_rect.left, Val::Px(10.0));
/// assert_eq!(ui_rect.right, Val::Px(10.0));
/// assert_eq!(ui_rect.top, Val::Px(10.0));
/// assert_eq!(ui_rect.bottom, Val::Px(10.0));
/// ```
pub const fn all(value: Val) -> Self {
UiRect {
left: value,
right: value,
top: value,
bottom: value,
}
}
/// Creates a new [`UiRect`] from the values specified in logical pixels.
///
/// This is a shortcut for [`UiRect::new()`], applying [`Val::Px`] to all arguments.
///
/// # Example
///
/// ```
/// # use bevy_ui::{UiRect, Val};
/// #
/// let ui_rect = UiRect::px(10., 20., 30., 40.);
/// assert_eq!(ui_rect.left, Val::Px(10.));
/// assert_eq!(ui_rect.right, Val::Px(20.));
/// assert_eq!(ui_rect.top, Val::Px(30.));
/// assert_eq!(ui_rect.bottom, Val::Px(40.));
/// ```
pub const fn px(left: f32, right: f32, top: f32, bottom: f32) -> Self {
UiRect {
left: Val::Px(left),
right: Val::Px(right),
top: Val::Px(top),
bottom: Val::Px(bottom),
}
}
/// Creates a new [`UiRect`] from the values specified in percentages.
///
/// This is a shortcut for [`UiRect::new()`], applying [`Val::Percent`] to all arguments.
///
/// # Example
///
/// ```
/// # use bevy_ui::{UiRect, Val};
/// #
/// let ui_rect = UiRect::percent(5., 10., 2., 1.);
/// assert_eq!(ui_rect.left, Val::Percent(5.));
/// assert_eq!(ui_rect.right, Val::Percent(10.));
/// assert_eq!(ui_rect.top, Val::Percent(2.));
/// assert_eq!(ui_rect.bottom, Val::Percent(1.));
/// ```
pub const fn percent(left: f32, right: f32, top: f32, bottom: f32) -> Self {
UiRect {
left: Val::Percent(left),
right: Val::Percent(right),
top: Val::Percent(top),
bottom: Val::Percent(bottom),
}
}
/// Creates a new [`UiRect`] where `left` and `right` take the given value,
/// and `top` and `bottom` set to zero `Val::ZERO`.
///
/// # Example
///
/// ```
/// # use bevy_ui::{UiRect, Val};
/// #
/// let ui_rect = UiRect::horizontal(Val::Px(10.0));
///
/// assert_eq!(ui_rect.left, Val::Px(10.0));
/// assert_eq!(ui_rect.right, Val::Px(10.0));
/// assert_eq!(ui_rect.top, Val::ZERO);
/// assert_eq!(ui_rect.bottom, Val::ZERO);
/// ```
pub const fn horizontal(value: Val) -> Self {
Self {
left: value,
right: value,
..Self::DEFAULT
}
}
/// Creates a new [`UiRect`] where `top` and `bottom` take the given value,
/// and `left` and `right` are set to `Val::ZERO`.
///
/// # Example
///
/// ```
/// # use bevy_ui::{UiRect, Val};
/// #
/// let ui_rect = UiRect::vertical(Val::Px(10.0));
///
/// assert_eq!(ui_rect.left, Val::ZERO);
/// assert_eq!(ui_rect.right, Val::ZERO);
/// assert_eq!(ui_rect.top, Val::Px(10.0));
/// assert_eq!(ui_rect.bottom, Val::Px(10.0));
/// ```
pub const fn vertical(value: Val) -> Self {
Self {
top: value,
bottom: value,
..Self::DEFAULT
}
}
/// Creates a new [`UiRect`] where both `left` and `right` take the value of `horizontal`, and both `top` and `bottom` take the value of `vertical`.
///
/// # Example
///
/// ```
/// # use bevy_ui::{UiRect, Val};
/// #
/// let ui_rect = UiRect::axes(Val::Px(10.0), Val::Percent(15.0));
///
/// assert_eq!(ui_rect.left, Val::Px(10.0));
/// assert_eq!(ui_rect.right, Val::Px(10.0));
/// assert_eq!(ui_rect.top, Val::Percent(15.0));
/// assert_eq!(ui_rect.bottom, Val::Percent(15.0));
/// ```
pub const fn axes(horizontal: Val, vertical: Val) -> Self {
Self {
left: horizontal,
right: horizontal,
top: vertical,
bottom: vertical,
}
}
/// Creates a new [`UiRect`] where `left` takes the given value, and
/// the other fields are set to `Val::ZERO`.
///
/// # Example
///
/// ```
/// # use bevy_ui::{UiRect, Val};
/// #
/// let ui_rect = UiRect::left(Val::Px(10.0));
///
/// assert_eq!(ui_rect.left, Val::Px(10.0));
/// assert_eq!(ui_rect.right, Val::ZERO);
/// assert_eq!(ui_rect.top, Val::ZERO);
/// assert_eq!(ui_rect.bottom, Val::ZERO);
/// ```
pub const fn left(left: Val) -> Self {
Self {
left,
..Self::DEFAULT
}
}
/// Creates a new [`UiRect`] where `right` takes the given value,
/// and the other fields are set to `Val::ZERO`.
///
/// # Example
///
/// ```
/// # use bevy_ui::{UiRect, Val};
/// #
/// let ui_rect = UiRect::right(Val::Px(10.0));
///
/// assert_eq!(ui_rect.left, Val::ZERO);
/// assert_eq!(ui_rect.right, Val::Px(10.0));
/// assert_eq!(ui_rect.top, Val::ZERO);
/// assert_eq!(ui_rect.bottom, Val::ZERO);
/// ```
pub const fn right(right: Val) -> Self {
Self {
right,
..Self::DEFAULT
}
}
/// Creates a new [`UiRect`] where `top` takes the given value,
/// and the other fields are set to `Val::ZERO`.
///
/// # Example
///
/// ```
/// # use bevy_ui::{UiRect, Val};
/// #
/// let ui_rect = UiRect::top(Val::Px(10.0));
///
/// assert_eq!(ui_rect.left, Val::ZERO);
/// assert_eq!(ui_rect.right, Val::ZERO);
/// assert_eq!(ui_rect.top, Val::Px(10.0));
/// assert_eq!(ui_rect.bottom, Val::ZERO);
/// ```
pub const fn top(top: Val) -> Self {
Self {
top,
..Self::DEFAULT
}
}
/// Creates a new [`UiRect`] where `bottom` takes the given value,
/// and the other fields are set to `Val::ZERO`.
///
/// # Example
///
/// ```
/// # use bevy_ui::{UiRect, Val};
/// #
/// let ui_rect = UiRect::bottom(Val::Px(10.0));
///
/// assert_eq!(ui_rect.left, Val::ZERO);
/// assert_eq!(ui_rect.right, Val::ZERO);
/// assert_eq!(ui_rect.top, Val::ZERO);
/// assert_eq!(ui_rect.bottom, Val::Px(10.0));
/// ```
pub const fn bottom(bottom: Val) -> Self {
Self {
bottom,
..Self::DEFAULT
}
}
/// Returns the [`UiRect`] with its `left` field set to the given value.
///
/// # Example
///
/// ```
/// # use bevy_ui::{UiRect, Val};
/// #
/// let ui_rect = UiRect::all(Val::Px(20.0)).with_left(Val::Px(10.0));
/// assert_eq!(ui_rect.left, Val::Px(10.0));
/// assert_eq!(ui_rect.right, Val::Px(20.0));
/// assert_eq!(ui_rect.top, Val::Px(20.0));
/// assert_eq!(ui_rect.bottom, Val::Px(20.0));
/// ```
#[inline]
pub const fn with_left(mut self, left: Val) -> Self {
self.left = left;
self
}
/// Returns the [`UiRect`] with its `right` field set to the given value.
///
/// # Example
///
/// ```
/// # use bevy_ui::{UiRect, Val};
/// #
/// let ui_rect = UiRect::all(Val::Px(20.0)).with_right(Val::Px(10.0));
/// assert_eq!(ui_rect.left, Val::Px(20.0));
/// assert_eq!(ui_rect.right, Val::Px(10.0));
/// assert_eq!(ui_rect.top, Val::Px(20.0));
/// assert_eq!(ui_rect.bottom, Val::Px(20.0));
/// ```
#[inline]
pub const fn with_right(mut self, right: Val) -> Self {
self.right = right;
self
}
/// Returns the [`UiRect`] with its `top` field set to the given value.
///
/// # Example
///
/// ```
/// # use bevy_ui::{UiRect, Val};
/// #
/// let ui_rect = UiRect::all(Val::Px(20.0)).with_top(Val::Px(10.0));
/// assert_eq!(ui_rect.left, Val::Px(20.0));
/// assert_eq!(ui_rect.right, Val::Px(20.0));
/// assert_eq!(ui_rect.top, Val::Px(10.0));
/// assert_eq!(ui_rect.bottom, Val::Px(20.0));
/// ```
#[inline]
pub const fn with_top(mut self, top: Val) -> Self {
self.top = top;
self
}
/// Returns the [`UiRect`] with its `bottom` field set to the given value.
///
/// # Example
///
/// ```
/// # use bevy_ui::{UiRect, Val};
/// #
/// let ui_rect = UiRect::all(Val::Px(20.0)).with_bottom(Val::Px(10.0));
/// assert_eq!(ui_rect.left, Val::Px(20.0));
/// assert_eq!(ui_rect.right, Val::Px(20.0));
/// assert_eq!(ui_rect.top, Val::Px(20.0));
/// assert_eq!(ui_rect.bottom, Val::Px(10.0));
/// ```
#[inline]
pub const fn with_bottom(mut self, bottom: Val) -> Self {
self.bottom = bottom;
self
}
}
impl Default for UiRect {
fn default() -> Self {
Self::DEFAULT
}
}
#[cfg(test)]
mod tests {
use crate::geometry::*;
use bevy_math::vec2;
#[test]
fn val_evaluate() {
let size = 250.;
let viewport_size = vec2(1000., 500.);
let result = Val::Percent(80.).resolve(size, viewport_size).unwrap();
assert_eq!(result, size * 0.8);
}
#[test]
fn val_resolve_px() {
let size = 250.;
let viewport_size = vec2(1000., 500.);
let result = Val::Px(10.).resolve(size, viewport_size).unwrap();
assert_eq!(result, 10.);
}
#[test]
fn val_resolve_viewport_coords() {
let size = 250.;
let viewport_size = vec2(500., 500.);
for value in (-10..10).map(|value| value as f32) {
// for a square viewport there should be no difference between `Vw` and `Vh` and between `Vmin` and `Vmax`.
assert_eq!(
Val::Vw(value).resolve(size, viewport_size),
Val::Vh(value).resolve(size, viewport_size)
);
assert_eq!(
Val::VMin(value).resolve(size, viewport_size),
Val::VMax(value).resolve(size, viewport_size)
);
assert_eq!(
Val::VMin(value).resolve(size, viewport_size),
Val::Vw(value).resolve(size, viewport_size)
);
}
let viewport_size = vec2(1000., 500.);
assert_eq!(Val::Vw(100.).resolve(size, viewport_size).unwrap(), 1000.);
assert_eq!(Val::Vh(100.).resolve(size, viewport_size).unwrap(), 500.);
assert_eq!(Val::Vw(60.).resolve(size, viewport_size).unwrap(), 600.);
assert_eq!(Val::Vh(40.).resolve(size, viewport_size).unwrap(), 200.);
assert_eq!(Val::VMin(50.).resolve(size, viewport_size).unwrap(), 250.);
assert_eq!(Val::VMax(75.).resolve(size, viewport_size).unwrap(), 750.);
}
#[test]
fn val_auto_is_non_evaluable() {
let size = 250.;
let viewport_size = vec2(1000., 500.);
let resolve_auto = Val::Auto.resolve(size, viewport_size);
assert_eq!(resolve_auto, Err(ValArithmeticError::NonEvaluable));
}
#[test]
fn val_arithmetic_error_messages() {
assert_eq!(
format!("{}", ValArithmeticError::NonEvaluable),
"the given variant of Val is not evaluable (non-numeric)"
);
}
#[test]
fn val_str_parse() {
assert_eq!("auto".parse::<Val>(), Ok(Val::Auto));
assert_eq!("Auto".parse::<Val>(), Ok(Val::Auto));
assert_eq!("AUTO".parse::<Val>(), Ok(Val::Auto));
assert_eq!("3px".parse::<Val>(), Ok(Val::Px(3.)));
assert_eq!("3 px".parse::<Val>(), Ok(Val::Px(3.)));
assert_eq!("3.5px".parse::<Val>(), Ok(Val::Px(3.5)));
assert_eq!("-3px".parse::<Val>(), Ok(Val::Px(-3.)));
assert_eq!("3.5 PX".parse::<Val>(), Ok(Val::Px(3.5)));
assert_eq!("3%".parse::<Val>(), Ok(Val::Percent(3.)));
assert_eq!("3 %".parse::<Val>(), Ok(Val::Percent(3.)));
assert_eq!("3.5%".parse::<Val>(), Ok(Val::Percent(3.5)));
assert_eq!("-3%".parse::<Val>(), Ok(Val::Percent(-3.)));
assert_eq!("3vw".parse::<Val>(), Ok(Val::Vw(3.)));
assert_eq!("3 vw".parse::<Val>(), Ok(Val::Vw(3.)));
assert_eq!("3.5vw".parse::<Val>(), Ok(Val::Vw(3.5)));
assert_eq!("-3vw".parse::<Val>(), Ok(Val::Vw(-3.)));
assert_eq!("3.5 VW".parse::<Val>(), Ok(Val::Vw(3.5)));
assert_eq!("3vh".parse::<Val>(), Ok(Val::Vh(3.)));
assert_eq!("3 vh".parse::<Val>(), Ok(Val::Vh(3.)));
assert_eq!("3.5vh".parse::<Val>(), Ok(Val::Vh(3.5)));
assert_eq!("-3vh".parse::<Val>(), Ok(Val::Vh(-3.)));
assert_eq!("3.5 VH".parse::<Val>(), Ok(Val::Vh(3.5)));
assert_eq!("3vmin".parse::<Val>(), Ok(Val::VMin(3.)));
assert_eq!("3 vmin".parse::<Val>(), Ok(Val::VMin(3.)));
assert_eq!("3.5vmin".parse::<Val>(), Ok(Val::VMin(3.5)));
assert_eq!("-3vmin".parse::<Val>(), Ok(Val::VMin(-3.)));
assert_eq!("3.5 VMIN".parse::<Val>(), Ok(Val::VMin(3.5)));
assert_eq!("3vmax".parse::<Val>(), Ok(Val::VMax(3.)));
assert_eq!("3 vmax".parse::<Val>(), Ok(Val::VMax(3.)));
assert_eq!("3.5vmax".parse::<Val>(), Ok(Val::VMax(3.5)));
assert_eq!("-3vmax".parse::<Val>(), Ok(Val::VMax(-3.)));
assert_eq!("3.5 VMAX".parse::<Val>(), Ok(Val::VMax(3.5)));
assert_eq!("".parse::<Val>(), Err(ValParseError::UnitMissing));
assert_eq!(
"hello world".parse::<Val>(),
Err(ValParseError::ValueMissing)
);
assert_eq!("3".parse::<Val>(), Err(ValParseError::UnitMissing));
assert_eq!("3.5".parse::<Val>(), Err(ValParseError::UnitMissing));
assert_eq!("3pxx".parse::<Val>(), Err(ValParseError::InvalidUnit));
assert_eq!("3.5pxx".parse::<Val>(), Err(ValParseError::InvalidUnit));
assert_eq!("3-3px".parse::<Val>(), Err(ValParseError::InvalidValue));
assert_eq!("3.5-3px".parse::<Val>(), Err(ValParseError::InvalidValue));
}
#[test]
fn default_val_equals_const_default_val() {
assert_eq!(Val::default(), Val::DEFAULT);
}
#[test]
fn uirect_default_equals_const_default() {
assert_eq!(UiRect::default(), UiRect::all(Val::ZERO));
assert_eq!(UiRect::default(), UiRect::DEFAULT);
}
#[test]
fn test_uirect_axes() {
let x = Val::Px(1.);
let y = Val::Vw(4.);
let r = UiRect::axes(x, y);
let h = UiRect::horizontal(x);
let v = UiRect::vertical(y);
assert_eq!(r.top, v.top);
assert_eq!(r.bottom, v.bottom);
assert_eq!(r.left, h.left);
assert_eq!(r.right, h.right);
}
#[test]
fn uirect_px() {
let r = UiRect::px(3., 5., 20., 999.);
assert_eq!(r.left, Val::Px(3.));
assert_eq!(r.right, Val::Px(5.));
assert_eq!(r.top, Val::Px(20.));
assert_eq!(r.bottom, Val::Px(999.));
}
#[test]
fn uirect_percent() {
let r = UiRect::percent(3., 5., 20., 99.);
assert_eq!(r.left, Val::Percent(3.));
assert_eq!(r.right, Val::Percent(5.));
assert_eq!(r.top, Val::Percent(20.));
assert_eq!(r.bottom, Val::Percent(99.));
}
}

683
vendor/bevy_ui/src/layout/convert.rs vendored Normal file
View File

@@ -0,0 +1,683 @@
use taffy::style_helpers;
use crate::{
AlignContent, AlignItems, AlignSelf, BoxSizing, Display, FlexDirection, FlexWrap, GridAutoFlow,
GridPlacement, GridTrack, GridTrackRepetition, JustifyContent, JustifyItems, JustifySelf,
MaxTrackSizingFunction, MinTrackSizingFunction, Node, OverflowAxis, PositionType,
RepeatedGridTrack, UiRect, Val,
};
use super::LayoutContext;
impl Val {
fn into_length_percentage_auto(
self,
context: &LayoutContext,
) -> taffy::style::LengthPercentageAuto {
match self {
Val::Auto => taffy::style::LengthPercentageAuto::Auto,
Val::Percent(value) => taffy::style::LengthPercentageAuto::Percent(value / 100.),
Val::Px(value) => {
taffy::style::LengthPercentageAuto::Length(context.scale_factor * value)
}
Val::VMin(value) => taffy::style::LengthPercentageAuto::Length(
context.physical_size.min_element() * value / 100.,
),
Val::VMax(value) => taffy::style::LengthPercentageAuto::Length(
context.physical_size.max_element() * value / 100.,
),
Val::Vw(value) => {
taffy::style::LengthPercentageAuto::Length(context.physical_size.x * value / 100.)
}
Val::Vh(value) => {
taffy::style::LengthPercentageAuto::Length(context.physical_size.y * value / 100.)
}
}
}
fn into_length_percentage(self, context: &LayoutContext) -> taffy::style::LengthPercentage {
match self.into_length_percentage_auto(context) {
taffy::style::LengthPercentageAuto::Auto => taffy::style::LengthPercentage::Length(0.0),
taffy::style::LengthPercentageAuto::Percent(value) => {
taffy::style::LengthPercentage::Percent(value)
}
taffy::style::LengthPercentageAuto::Length(value) => {
taffy::style::LengthPercentage::Length(value)
}
}
}
fn into_dimension(self, context: &LayoutContext) -> taffy::style::Dimension {
self.into_length_percentage_auto(context).into()
}
}
impl UiRect {
fn map_to_taffy_rect<T>(self, map_fn: impl Fn(Val) -> T) -> taffy::geometry::Rect<T> {
taffy::geometry::Rect {
left: map_fn(self.left),
right: map_fn(self.right),
top: map_fn(self.top),
bottom: map_fn(self.bottom),
}
}
}
pub fn from_node(node: &Node, context: &LayoutContext, ignore_border: bool) -> taffy::style::Style {
taffy::style::Style {
display: node.display.into(),
box_sizing: node.box_sizing.into(),
item_is_table: false,
text_align: taffy::TextAlign::Auto,
overflow: taffy::Point {
x: node.overflow.x.into(),
y: node.overflow.y.into(),
},
scrollbar_width: 0.0,
position: node.position_type.into(),
flex_direction: node.flex_direction.into(),
flex_wrap: node.flex_wrap.into(),
align_items: node.align_items.into(),
justify_items: node.justify_items.into(),
align_self: node.align_self.into(),
justify_self: node.justify_self.into(),
align_content: node.align_content.into(),
justify_content: node.justify_content.into(),
inset: taffy::Rect {
left: node.left.into_length_percentage_auto(context),
right: node.right.into_length_percentage_auto(context),
top: node.top.into_length_percentage_auto(context),
bottom: node.bottom.into_length_percentage_auto(context),
},
margin: node
.margin
.map_to_taffy_rect(|m| m.into_length_percentage_auto(context)),
padding: node
.padding
.map_to_taffy_rect(|m| m.into_length_percentage(context)),
// Ignore border for leaf nodes as it isn't implemented in the rendering engine.
// TODO: Implement rendering of border for leaf nodes
border: if ignore_border {
taffy::Rect::zero()
} else {
node.border
.map_to_taffy_rect(|m| m.into_length_percentage(context))
},
flex_grow: node.flex_grow,
flex_shrink: node.flex_shrink,
flex_basis: node.flex_basis.into_dimension(context),
size: taffy::Size {
width: node.width.into_dimension(context),
height: node.height.into_dimension(context),
},
min_size: taffy::Size {
width: node.min_width.into_dimension(context),
height: node.min_height.into_dimension(context),
},
max_size: taffy::Size {
width: node.max_width.into_dimension(context),
height: node.max_height.into_dimension(context),
},
aspect_ratio: node.aspect_ratio,
gap: taffy::Size {
width: node.column_gap.into_length_percentage(context),
height: node.row_gap.into_length_percentage(context),
},
grid_auto_flow: node.grid_auto_flow.into(),
grid_template_rows: node
.grid_template_rows
.iter()
.map(|track| track.clone_into_repeated_taffy_track(context))
.collect::<Vec<_>>(),
grid_template_columns: node
.grid_template_columns
.iter()
.map(|track| track.clone_into_repeated_taffy_track(context))
.collect::<Vec<_>>(),
grid_auto_rows: node
.grid_auto_rows
.iter()
.map(|track| track.into_taffy_track(context))
.collect::<Vec<_>>(),
grid_auto_columns: node
.grid_auto_columns
.iter()
.map(|track| track.into_taffy_track(context))
.collect::<Vec<_>>(),
grid_row: node.grid_row.into(),
grid_column: node.grid_column.into(),
}
}
impl From<AlignItems> for Option<taffy::style::AlignItems> {
fn from(value: AlignItems) -> Self {
match value {
AlignItems::Default => None,
AlignItems::Start => taffy::style::AlignItems::Start.into(),
AlignItems::End => taffy::style::AlignItems::End.into(),
AlignItems::FlexStart => taffy::style::AlignItems::FlexStart.into(),
AlignItems::FlexEnd => taffy::style::AlignItems::FlexEnd.into(),
AlignItems::Center => taffy::style::AlignItems::Center.into(),
AlignItems::Baseline => taffy::style::AlignItems::Baseline.into(),
AlignItems::Stretch => taffy::style::AlignItems::Stretch.into(),
}
}
}
impl From<JustifyItems> for Option<taffy::style::JustifyItems> {
fn from(value: JustifyItems) -> Self {
match value {
JustifyItems::Default => None,
JustifyItems::Start => taffy::style::JustifyItems::Start.into(),
JustifyItems::End => taffy::style::JustifyItems::End.into(),
JustifyItems::Center => taffy::style::JustifyItems::Center.into(),
JustifyItems::Baseline => taffy::style::JustifyItems::Baseline.into(),
JustifyItems::Stretch => taffy::style::JustifyItems::Stretch.into(),
}
}
}
impl From<AlignSelf> for Option<taffy::style::AlignSelf> {
fn from(value: AlignSelf) -> Self {
match value {
AlignSelf::Auto => None,
AlignSelf::Start => taffy::style::AlignSelf::Start.into(),
AlignSelf::End => taffy::style::AlignSelf::End.into(),
AlignSelf::FlexStart => taffy::style::AlignSelf::FlexStart.into(),
AlignSelf::FlexEnd => taffy::style::AlignSelf::FlexEnd.into(),
AlignSelf::Center => taffy::style::AlignSelf::Center.into(),
AlignSelf::Baseline => taffy::style::AlignSelf::Baseline.into(),
AlignSelf::Stretch => taffy::style::AlignSelf::Stretch.into(),
}
}
}
impl From<JustifySelf> for Option<taffy::style::JustifySelf> {
fn from(value: JustifySelf) -> Self {
match value {
JustifySelf::Auto => None,
JustifySelf::Start => taffy::style::JustifySelf::Start.into(),
JustifySelf::End => taffy::style::JustifySelf::End.into(),
JustifySelf::Center => taffy::style::JustifySelf::Center.into(),
JustifySelf::Baseline => taffy::style::JustifySelf::Baseline.into(),
JustifySelf::Stretch => taffy::style::JustifySelf::Stretch.into(),
}
}
}
impl From<AlignContent> for Option<taffy::style::AlignContent> {
fn from(value: AlignContent) -> Self {
match value {
AlignContent::Default => None,
AlignContent::Start => taffy::style::AlignContent::Start.into(),
AlignContent::End => taffy::style::AlignContent::End.into(),
AlignContent::FlexStart => taffy::style::AlignContent::FlexStart.into(),
AlignContent::FlexEnd => taffy::style::AlignContent::FlexEnd.into(),
AlignContent::Center => taffy::style::AlignContent::Center.into(),
AlignContent::Stretch => taffy::style::AlignContent::Stretch.into(),
AlignContent::SpaceBetween => taffy::style::AlignContent::SpaceBetween.into(),
AlignContent::SpaceAround => taffy::style::AlignContent::SpaceAround.into(),
AlignContent::SpaceEvenly => taffy::style::AlignContent::SpaceEvenly.into(),
}
}
}
impl From<JustifyContent> for Option<taffy::style::JustifyContent> {
fn from(value: JustifyContent) -> Self {
match value {
JustifyContent::Default => None,
JustifyContent::Start => taffy::style::JustifyContent::Start.into(),
JustifyContent::End => taffy::style::JustifyContent::End.into(),
JustifyContent::FlexStart => taffy::style::JustifyContent::FlexStart.into(),
JustifyContent::FlexEnd => taffy::style::JustifyContent::FlexEnd.into(),
JustifyContent::Center => taffy::style::JustifyContent::Center.into(),
JustifyContent::Stretch => taffy::style::JustifyContent::Stretch.into(),
JustifyContent::SpaceBetween => taffy::style::JustifyContent::SpaceBetween.into(),
JustifyContent::SpaceAround => taffy::style::JustifyContent::SpaceAround.into(),
JustifyContent::SpaceEvenly => taffy::style::JustifyContent::SpaceEvenly.into(),
}
}
}
impl From<Display> for taffy::style::Display {
fn from(value: Display) -> Self {
match value {
Display::Flex => taffy::style::Display::Flex,
Display::Grid => taffy::style::Display::Grid,
Display::Block => taffy::style::Display::Block,
Display::None => taffy::style::Display::None,
}
}
}
impl From<BoxSizing> for taffy::style::BoxSizing {
fn from(value: BoxSizing) -> Self {
match value {
BoxSizing::BorderBox => taffy::style::BoxSizing::BorderBox,
BoxSizing::ContentBox => taffy::style::BoxSizing::ContentBox,
}
}
}
impl From<OverflowAxis> for taffy::style::Overflow {
fn from(value: OverflowAxis) -> Self {
match value {
OverflowAxis::Visible => taffy::style::Overflow::Visible,
OverflowAxis::Clip => taffy::style::Overflow::Clip,
OverflowAxis::Hidden => taffy::style::Overflow::Hidden,
OverflowAxis::Scroll => taffy::style::Overflow::Scroll,
}
}
}
impl From<FlexDirection> for taffy::style::FlexDirection {
fn from(value: FlexDirection) -> Self {
match value {
FlexDirection::Row => taffy::style::FlexDirection::Row,
FlexDirection::Column => taffy::style::FlexDirection::Column,
FlexDirection::RowReverse => taffy::style::FlexDirection::RowReverse,
FlexDirection::ColumnReverse => taffy::style::FlexDirection::ColumnReverse,
}
}
}
impl From<PositionType> for taffy::style::Position {
fn from(value: PositionType) -> Self {
match value {
PositionType::Relative => taffy::style::Position::Relative,
PositionType::Absolute => taffy::style::Position::Absolute,
}
}
}
impl From<FlexWrap> for taffy::style::FlexWrap {
fn from(value: FlexWrap) -> Self {
match value {
FlexWrap::NoWrap => taffy::style::FlexWrap::NoWrap,
FlexWrap::Wrap => taffy::style::FlexWrap::Wrap,
FlexWrap::WrapReverse => taffy::style::FlexWrap::WrapReverse,
}
}
}
impl From<GridAutoFlow> for taffy::style::GridAutoFlow {
fn from(value: GridAutoFlow) -> Self {
match value {
GridAutoFlow::Row => taffy::style::GridAutoFlow::Row,
GridAutoFlow::RowDense => taffy::style::GridAutoFlow::RowDense,
GridAutoFlow::Column => taffy::style::GridAutoFlow::Column,
GridAutoFlow::ColumnDense => taffy::style::GridAutoFlow::ColumnDense,
}
}
}
impl From<GridPlacement> for taffy::geometry::Line<taffy::style::GridPlacement> {
fn from(value: GridPlacement) -> Self {
let span = value.get_span().unwrap_or(1);
match (value.get_start(), value.get_end()) {
(Some(start), Some(end)) => taffy::geometry::Line {
start: style_helpers::line(start),
end: style_helpers::line(end),
},
(Some(start), None) => taffy::geometry::Line {
start: style_helpers::line(start),
end: style_helpers::span(span),
},
(None, Some(end)) => taffy::geometry::Line {
start: style_helpers::span(span),
end: style_helpers::line(end),
},
(None, None) => style_helpers::span(span),
}
}
}
impl MinTrackSizingFunction {
fn into_taffy(self, context: &LayoutContext) -> taffy::style::MinTrackSizingFunction {
match self {
MinTrackSizingFunction::Px(val) => taffy::style::MinTrackSizingFunction::Fixed(
Val::Px(val).into_length_percentage(context),
),
MinTrackSizingFunction::Percent(val) => taffy::style::MinTrackSizingFunction::Fixed(
Val::Percent(val).into_length_percentage(context),
),
MinTrackSizingFunction::Auto => taffy::style::MinTrackSizingFunction::Auto,
MinTrackSizingFunction::MinContent => taffy::style::MinTrackSizingFunction::MinContent,
MinTrackSizingFunction::MaxContent => taffy::style::MinTrackSizingFunction::MaxContent,
MinTrackSizingFunction::VMin(val) => taffy::style::MinTrackSizingFunction::Fixed(
Val::VMin(val).into_length_percentage(context),
),
MinTrackSizingFunction::VMax(val) => taffy::style::MinTrackSizingFunction::Fixed(
Val::VMax(val).into_length_percentage(context),
),
MinTrackSizingFunction::Vh(val) => taffy::style::MinTrackSizingFunction::Fixed(
Val::Vh(val).into_length_percentage(context),
),
MinTrackSizingFunction::Vw(val) => taffy::style::MinTrackSizingFunction::Fixed(
Val::Vw(val).into_length_percentage(context),
),
}
}
}
impl MaxTrackSizingFunction {
fn into_taffy(self, context: &LayoutContext) -> taffy::style::MaxTrackSizingFunction {
match self {
MaxTrackSizingFunction::Px(val) => taffy::style::MaxTrackSizingFunction::Fixed(
Val::Px(val).into_length_percentage(context),
),
MaxTrackSizingFunction::Percent(val) => taffy::style::MaxTrackSizingFunction::Fixed(
Val::Percent(val).into_length_percentage(context),
),
MaxTrackSizingFunction::Auto => taffy::style::MaxTrackSizingFunction::Auto,
MaxTrackSizingFunction::MinContent => taffy::style::MaxTrackSizingFunction::MinContent,
MaxTrackSizingFunction::MaxContent => taffy::style::MaxTrackSizingFunction::MaxContent,
MaxTrackSizingFunction::FitContentPx(val) => {
taffy::style::MaxTrackSizingFunction::FitContent(
Val::Px(val).into_length_percentage(context),
)
}
MaxTrackSizingFunction::FitContentPercent(val) => {
taffy::style::MaxTrackSizingFunction::FitContent(
Val::Percent(val).into_length_percentage(context),
)
}
MaxTrackSizingFunction::Fraction(fraction) => {
taffy::style::MaxTrackSizingFunction::Fraction(fraction)
}
MaxTrackSizingFunction::VMin(val) => taffy::style::MaxTrackSizingFunction::Fixed(
Val::VMin(val).into_length_percentage(context),
),
MaxTrackSizingFunction::VMax(val) => taffy::style::MaxTrackSizingFunction::Fixed(
Val::VMax(val).into_length_percentage(context),
),
MaxTrackSizingFunction::Vh(val) => taffy::style::MaxTrackSizingFunction::Fixed(
Val::Vh(val).into_length_percentage(context),
),
MaxTrackSizingFunction::Vw(val) => taffy::style::MaxTrackSizingFunction::Fixed(
Val::Vw(val).into_length_percentage(context),
),
}
}
}
impl GridTrack {
fn into_taffy_track(
self,
context: &LayoutContext,
) -> taffy::style::NonRepeatedTrackSizingFunction {
let min = self.min_sizing_function.into_taffy(context);
let max = self.max_sizing_function.into_taffy(context);
style_helpers::minmax(min, max)
}
}
impl RepeatedGridTrack {
fn clone_into_repeated_taffy_track(
&self,
context: &LayoutContext,
) -> taffy::style::TrackSizingFunction {
if self.tracks.len() == 1 && self.repetition == GridTrackRepetition::Count(1) {
let min = self.tracks[0].min_sizing_function.into_taffy(context);
let max = self.tracks[0].max_sizing_function.into_taffy(context);
let taffy_track = style_helpers::minmax(min, max);
taffy::style::TrackSizingFunction::Single(taffy_track)
} else {
let taffy_tracks: Vec<_> = self
.tracks
.iter()
.map(|track| {
let min = track.min_sizing_function.into_taffy(context);
let max = track.max_sizing_function.into_taffy(context);
style_helpers::minmax(min, max)
})
.collect();
match self.repetition {
GridTrackRepetition::Count(count) => style_helpers::repeat(count, taffy_tracks),
GridTrackRepetition::AutoFit => {
style_helpers::repeat(taffy::style::GridTrackRepetition::AutoFit, taffy_tracks)
}
GridTrackRepetition::AutoFill => {
style_helpers::repeat(taffy::style::GridTrackRepetition::AutoFill, taffy_tracks)
}
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_convert_from() {
use sh::TaffyZero;
use taffy::style_helpers as sh;
let node = Node {
display: Display::Flex,
box_sizing: BoxSizing::ContentBox,
position_type: PositionType::Absolute,
left: Val::ZERO,
right: Val::Percent(50.),
top: Val::Px(12.),
bottom: Val::Auto,
flex_direction: FlexDirection::ColumnReverse,
flex_wrap: FlexWrap::WrapReverse,
align_items: AlignItems::Baseline,
align_self: AlignSelf::Start,
align_content: AlignContent::SpaceAround,
justify_items: JustifyItems::Default,
justify_self: JustifySelf::Center,
justify_content: JustifyContent::SpaceEvenly,
margin: UiRect {
left: Val::ZERO,
right: Val::Px(10.),
top: Val::Percent(15.),
bottom: Val::Auto,
},
padding: UiRect {
left: Val::Percent(13.),
right: Val::Px(21.),
top: Val::Auto,
bottom: Val::ZERO,
},
border: UiRect {
left: Val::Px(14.),
right: Val::ZERO,
top: Val::Auto,
bottom: Val::Percent(31.),
},
flex_grow: 1.,
flex_shrink: 0.,
flex_basis: Val::ZERO,
width: Val::ZERO,
height: Val::Auto,
min_width: Val::ZERO,
min_height: Val::ZERO,
max_width: Val::Auto,
max_height: Val::ZERO,
aspect_ratio: None,
overflow: crate::Overflow::clip(),
overflow_clip_margin: crate::OverflowClipMargin::default(),
column_gap: Val::ZERO,
row_gap: Val::ZERO,
grid_auto_flow: GridAutoFlow::ColumnDense,
grid_template_rows: vec![
GridTrack::px(10.0),
GridTrack::percent(50.0),
GridTrack::fr(1.0),
],
grid_template_columns: RepeatedGridTrack::px(5, 10.0),
grid_auto_rows: vec![
GridTrack::fit_content_px(10.0),
GridTrack::fit_content_percent(25.0),
GridTrack::flex(2.0),
],
grid_auto_columns: vec![
GridTrack::auto(),
GridTrack::min_content(),
GridTrack::max_content(),
],
grid_column: GridPlacement::start(4),
grid_row: GridPlacement::span(3),
};
let viewport_values = LayoutContext::new(1.0, bevy_math::Vec2::new(800., 600.));
let taffy_style = from_node(&node, &viewport_values, false);
assert_eq!(taffy_style.display, taffy::style::Display::Flex);
assert_eq!(taffy_style.box_sizing, taffy::style::BoxSizing::ContentBox);
assert_eq!(taffy_style.position, taffy::style::Position::Absolute);
assert_eq!(
taffy_style.inset.left,
taffy::style::LengthPercentageAuto::ZERO
);
assert_eq!(
taffy_style.inset.right,
taffy::style::LengthPercentageAuto::Percent(0.5)
);
assert_eq!(
taffy_style.inset.top,
taffy::style::LengthPercentageAuto::Length(12.)
);
assert_eq!(
taffy_style.inset.bottom,
taffy::style::LengthPercentageAuto::Auto
);
assert_eq!(
taffy_style.flex_direction,
taffy::style::FlexDirection::ColumnReverse
);
assert_eq!(taffy_style.flex_wrap, taffy::style::FlexWrap::WrapReverse);
assert_eq!(
taffy_style.align_items,
Some(taffy::style::AlignItems::Baseline)
);
assert_eq!(taffy_style.align_self, Some(taffy::style::AlignSelf::Start));
assert_eq!(
taffy_style.align_content,
Some(taffy::style::AlignContent::SpaceAround)
);
assert_eq!(
taffy_style.justify_content,
Some(taffy::style::JustifyContent::SpaceEvenly)
);
assert_eq!(taffy_style.justify_items, None);
assert_eq!(
taffy_style.justify_self,
Some(taffy::style::JustifySelf::Center)
);
assert_eq!(
taffy_style.margin.left,
taffy::style::LengthPercentageAuto::ZERO
);
assert_eq!(
taffy_style.margin.right,
taffy::style::LengthPercentageAuto::Length(10.)
);
assert_eq!(
taffy_style.margin.top,
taffy::style::LengthPercentageAuto::Percent(0.15)
);
assert_eq!(
taffy_style.margin.bottom,
taffy::style::LengthPercentageAuto::Auto
);
assert_eq!(
taffy_style.padding.left,
taffy::style::LengthPercentage::Percent(0.13)
);
assert_eq!(
taffy_style.padding.right,
taffy::style::LengthPercentage::Length(21.)
);
assert_eq!(
taffy_style.padding.top,
taffy::style::LengthPercentage::ZERO
);
assert_eq!(
taffy_style.padding.bottom,
taffy::style::LengthPercentage::ZERO
);
assert_eq!(
taffy_style.border.left,
taffy::style::LengthPercentage::Length(14.)
);
assert_eq!(
taffy_style.border.right,
taffy::style::LengthPercentage::ZERO
);
assert_eq!(taffy_style.border.top, taffy::style::LengthPercentage::ZERO);
assert_eq!(
taffy_style.border.bottom,
taffy::style::LengthPercentage::Percent(0.31)
);
assert_eq!(taffy_style.flex_grow, 1.);
assert_eq!(taffy_style.flex_shrink, 0.);
assert_eq!(taffy_style.flex_basis, taffy::style::Dimension::ZERO);
assert_eq!(taffy_style.size.width, taffy::style::Dimension::ZERO);
assert_eq!(taffy_style.size.height, taffy::style::Dimension::Auto);
assert_eq!(taffy_style.min_size.width, taffy::style::Dimension::ZERO);
assert_eq!(taffy_style.min_size.height, taffy::style::Dimension::ZERO);
assert_eq!(taffy_style.max_size.width, taffy::style::Dimension::Auto);
assert_eq!(taffy_style.max_size.height, taffy::style::Dimension::ZERO);
assert_eq!(taffy_style.aspect_ratio, None);
assert_eq!(taffy_style.gap.width, taffy::style::LengthPercentage::ZERO);
assert_eq!(taffy_style.gap.height, taffy::style::LengthPercentage::ZERO);
assert_eq!(
taffy_style.grid_auto_flow,
taffy::style::GridAutoFlow::ColumnDense
);
assert_eq!(
taffy_style.grid_template_rows,
vec![sh::length(10.0), sh::percent(0.5), sh::fr(1.0)]
);
assert_eq!(
taffy_style.grid_template_columns,
vec![sh::repeat(5, vec![sh::length(10.0)])]
);
assert_eq!(
taffy_style.grid_auto_rows,
vec![
sh::fit_content(taffy::style::LengthPercentage::Length(10.0)),
sh::fit_content(taffy::style::LengthPercentage::Percent(0.25)),
sh::minmax(sh::length(0.0), sh::fr(2.0)),
]
);
assert_eq!(
taffy_style.grid_auto_columns,
vec![sh::auto(), sh::min_content(), sh::max_content()]
);
assert_eq!(
taffy_style.grid_column,
taffy::geometry::Line {
start: sh::line(4),
end: sh::span(1)
}
);
assert_eq!(taffy_style.grid_row, sh::span(3));
}
#[test]
fn test_into_length_percentage() {
use taffy::style::LengthPercentage;
let context = LayoutContext::new(2.0, bevy_math::Vec2::new(800., 600.));
let cases = [
(Val::Auto, LengthPercentage::Length(0.)),
(Val::Percent(1.), LengthPercentage::Percent(0.01)),
(Val::Px(1.), LengthPercentage::Length(2.)),
(Val::Vw(1.), LengthPercentage::Length(8.)),
(Val::Vh(1.), LengthPercentage::Length(6.)),
(Val::VMin(2.), LengthPercentage::Length(12.)),
(Val::VMax(2.), LengthPercentage::Length(16.)),
];
for (val, length) in cases {
assert!(match (val.into_length_percentage(&context), length) {
(LengthPercentage::Length(a), LengthPercentage::Length(b))
| (LengthPercentage::Percent(a), LengthPercentage::Percent(b)) =>
(a - b).abs() < 0.0001,
_ => false,
});
}
}
}

91
vendor/bevy_ui/src/layout/debug.rs vendored Normal file
View File

@@ -0,0 +1,91 @@
use core::fmt::Write;
use taffy::{NodeId, TraversePartialTree};
use bevy_ecs::prelude::Entity;
use bevy_platform::collections::HashMap;
use crate::layout::ui_surface::UiSurface;
/// Prints a debug representation of the computed layout of the UI layout tree for each window.
pub fn print_ui_layout_tree(ui_surface: &UiSurface) {
let taffy_to_entity: HashMap<NodeId, Entity> = ui_surface
.entity_to_taffy
.iter()
.map(|(entity, node)| (node.id, *entity))
.collect();
for (&entity, &viewport_node) in &ui_surface.root_entity_to_viewport_node {
let mut out = String::new();
print_node(
ui_surface,
&taffy_to_entity,
entity,
viewport_node,
false,
String::new(),
&mut out,
);
tracing::info!("Layout tree for camera entity: {entity}\n{out}");
}
}
/// Recursively navigates the layout tree printing each node's information.
fn print_node(
ui_surface: &UiSurface,
taffy_to_entity: &HashMap<NodeId, Entity>,
entity: Entity,
node: NodeId,
has_sibling: bool,
lines_string: String,
acc: &mut String,
) {
let tree = &ui_surface.taffy;
let layout = tree.layout(node).unwrap();
let style = tree.style(node).unwrap();
let num_children = tree.child_count(node);
let display_variant = match (num_children, style.display) {
(_, taffy::style::Display::None) => "NONE",
(0, _) => "LEAF",
(_, taffy::style::Display::Flex) => "FLEX",
(_, taffy::style::Display::Grid) => "GRID",
(_, taffy::style::Display::Block) => "BLOCK",
};
let fork_string = if has_sibling {
"├── "
} else {
"└── "
};
writeln!(
acc,
"{lines}{fork} {display} [x: {x:<4} y: {y:<4} width: {width:<4} height: {height:<4}] ({entity}) {measured}",
lines = lines_string,
fork = fork_string,
display = display_variant,
x = layout.location.x,
y = layout.location.y,
width = layout.size.width,
height = layout.size.height,
measured = if tree.get_node_context(node).is_some() { "measured" } else { "" }
).ok();
let bar = if has_sibling { "" } else { " " };
let new_string = lines_string + bar;
// Recurse into children
for (index, child_node) in tree.children(node).unwrap().iter().enumerate() {
let has_sibling = index < num_children - 1;
let child_entity = taffy_to_entity.get(child_node).unwrap();
print_node(
ui_surface,
taffy_to_entity,
*child_entity,
*child_node,
has_sibling,
new_string.clone(),
acc,
);
}
}

1140
vendor/bevy_ui/src/layout/mod.rs vendored Normal file

File diff suppressed because it is too large Load Diff

469
vendor/bevy_ui/src/layout/ui_surface.rs vendored Normal file
View File

@@ -0,0 +1,469 @@
use core::fmt;
use bevy_platform::collections::hash_map::Entry;
use taffy::TaffyTree;
use bevy_ecs::{
entity::{Entity, EntityHashMap},
prelude::Resource,
};
use bevy_math::{UVec2, Vec2};
use bevy_utils::default;
use crate::{layout::convert, LayoutContext, LayoutError, Measure, MeasureArgs, Node, NodeMeasure};
use bevy_text::CosmicFontSystem;
#[derive(Debug, Copy, Clone, PartialEq, Eq)]
pub struct LayoutNode {
// Implicit "viewport" node if this `LayoutNode` corresponds to a root UI node entity
pub(super) viewport_id: Option<taffy::NodeId>,
// The id of the node in the taffy tree
pub(super) id: taffy::NodeId,
}
impl From<taffy::NodeId> for LayoutNode {
fn from(value: taffy::NodeId) -> Self {
LayoutNode {
viewport_id: None,
id: value,
}
}
}
#[derive(Resource)]
pub struct UiSurface {
pub root_entity_to_viewport_node: EntityHashMap<taffy::NodeId>,
pub(super) entity_to_taffy: EntityHashMap<LayoutNode>,
pub(super) taffy: TaffyTree<NodeMeasure>,
taffy_children_scratch: Vec<taffy::NodeId>,
}
fn _assert_send_sync_ui_surface_impl_safe() {
fn _assert_send_sync<T: Send + Sync>() {}
_assert_send_sync::<EntityHashMap<taffy::NodeId>>();
_assert_send_sync::<TaffyTree<NodeMeasure>>();
_assert_send_sync::<UiSurface>();
}
impl fmt::Debug for UiSurface {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
f.debug_struct("UiSurface")
.field("entity_to_taffy", &self.entity_to_taffy)
.field("taffy_children_scratch", &self.taffy_children_scratch)
.finish()
}
}
impl Default for UiSurface {
fn default() -> Self {
let taffy: TaffyTree<NodeMeasure> = TaffyTree::new();
Self {
root_entity_to_viewport_node: Default::default(),
entity_to_taffy: Default::default(),
taffy,
taffy_children_scratch: Vec::new(),
}
}
}
impl UiSurface {
/// Retrieves the Taffy node associated with the given UI node entity and updates its style.
/// If no associated Taffy node exists a new Taffy node is inserted into the Taffy layout.
pub fn upsert_node(
&mut self,
layout_context: &LayoutContext,
entity: Entity,
node: &Node,
mut new_node_context: Option<NodeMeasure>,
) {
let taffy = &mut self.taffy;
match self.entity_to_taffy.entry(entity) {
Entry::Occupied(entry) => {
let taffy_node = *entry.get();
let has_measure = if new_node_context.is_some() {
taffy
.set_node_context(taffy_node.id, new_node_context)
.unwrap();
true
} else {
taffy.get_node_context(taffy_node.id).is_some()
};
taffy
.set_style(
taffy_node.id,
convert::from_node(node, layout_context, has_measure),
)
.unwrap();
}
Entry::Vacant(entry) => {
let taffy_node = if let Some(measure) = new_node_context.take() {
taffy.new_leaf_with_context(
convert::from_node(node, layout_context, true),
measure,
)
} else {
taffy.new_leaf(convert::from_node(node, layout_context, false))
};
entry.insert(taffy_node.unwrap().into());
}
}
}
/// Update the `MeasureFunc` of the taffy node corresponding to the given [`Entity`] if the node exists.
pub fn update_node_context(&mut self, entity: Entity, context: NodeMeasure) -> Option<()> {
let taffy_node = self.entity_to_taffy.get(&entity)?;
self.taffy
.set_node_context(taffy_node.id, Some(context))
.ok()
}
/// Update the children of the taffy node corresponding to the given [`Entity`].
pub fn update_children(&mut self, entity: Entity, children: impl Iterator<Item = Entity>) {
self.taffy_children_scratch.clear();
for child in children {
if let Some(taffy_node) = self.entity_to_taffy.get_mut(&child) {
self.taffy_children_scratch.push(taffy_node.id);
if let Some(viewport_id) = taffy_node.viewport_id.take() {
self.taffy.remove(viewport_id).ok();
}
}
}
let taffy_node = self.entity_to_taffy.get(&entity).unwrap();
self.taffy
.set_children(taffy_node.id, &self.taffy_children_scratch)
.unwrap();
}
/// Removes children from the entity's taffy node if it exists. Does nothing otherwise.
pub fn try_remove_children(&mut self, entity: Entity) {
if let Some(taffy_node) = self.entity_to_taffy.get(&entity) {
self.taffy.set_children(taffy_node.id, &[]).unwrap();
}
}
/// Removes the measure from the entity's taffy node if it exists. Does nothing otherwise.
pub fn try_remove_node_context(&mut self, entity: Entity) {
if let Some(taffy_node) = self.entity_to_taffy.get(&entity) {
self.taffy.set_node_context(taffy_node.id, None).unwrap();
}
}
/// Gets or inserts an implicit taffy viewport node corresponding to the given UI root entity
pub fn get_or_insert_taffy_viewport_node(&mut self, ui_root_entity: Entity) -> taffy::NodeId {
*self
.root_entity_to_viewport_node
.entry(ui_root_entity)
.or_insert_with(|| {
let root_node = self.entity_to_taffy.get_mut(&ui_root_entity).unwrap();
let implicit_root = self
.taffy
.new_leaf(taffy::style::Style {
display: taffy::style::Display::Grid,
// Note: Taffy percentages are floats ranging from 0.0 to 1.0.
// So this is setting width:100% and height:100%
size: taffy::geometry::Size {
width: taffy::style::Dimension::Percent(1.0),
height: taffy::style::Dimension::Percent(1.0),
},
align_items: Some(taffy::style::AlignItems::Start),
justify_items: Some(taffy::style::JustifyItems::Start),
..default()
})
.unwrap();
self.taffy.add_child(implicit_root, root_node.id).unwrap();
root_node.viewport_id = Some(implicit_root);
implicit_root
})
}
/// Compute the layout for the given implicit taffy viewport node
pub fn compute_layout<'a>(
&mut self,
ui_root_entity: Entity,
render_target_resolution: UVec2,
buffer_query: &'a mut bevy_ecs::prelude::Query<&mut bevy_text::ComputedTextBlock>,
font_system: &'a mut CosmicFontSystem,
) {
let implicit_viewport_node = self.get_or_insert_taffy_viewport_node(ui_root_entity);
let available_space = taffy::geometry::Size {
width: taffy::style::AvailableSpace::Definite(render_target_resolution.x as f32),
height: taffy::style::AvailableSpace::Definite(render_target_resolution.y as f32),
};
self.taffy
.compute_layout_with_measure(
implicit_viewport_node,
available_space,
|known_dimensions: taffy::Size<Option<f32>>,
available_space: taffy::Size<taffy::AvailableSpace>,
_node_id: taffy::NodeId,
context: Option<&mut NodeMeasure>,
style: &taffy::Style|
-> taffy::Size<f32> {
context
.map(|ctx| {
let buffer = get_text_buffer(
crate::widget::TextMeasure::needs_buffer(
known_dimensions.height,
available_space.width,
),
ctx,
buffer_query,
);
let size = ctx.measure(
MeasureArgs {
width: known_dimensions.width,
height: known_dimensions.height,
available_width: available_space.width,
available_height: available_space.height,
font_system,
buffer,
},
style,
);
taffy::Size {
width: size.x,
height: size.y,
}
})
.unwrap_or(taffy::Size::ZERO)
},
)
.unwrap();
}
/// Removes each entity from the internal map and then removes their associated nodes from taffy
pub fn remove_entities(&mut self, entities: impl IntoIterator<Item = Entity>) {
for entity in entities {
if let Some(node) = self.entity_to_taffy.remove(&entity) {
self.taffy.remove(node.id).unwrap();
if let Some(viewport_node) = node.viewport_id {
self.taffy.remove(viewport_node).ok();
}
}
}
}
/// Get the layout geometry for the taffy node corresponding to the ui node [`Entity`].
/// Does not compute the layout geometry, `compute_window_layouts` should be run before using this function.
/// On success returns a pair consisting of the final resolved layout values after rounding
/// and the size of the node after layout resolution but before rounding.
pub fn get_layout(
&mut self,
entity: Entity,
use_rounding: bool,
) -> Result<(taffy::Layout, Vec2), LayoutError> {
let Some(taffy_node) = self.entity_to_taffy.get(&entity) else {
return Err(LayoutError::InvalidHierarchy);
};
if use_rounding {
self.taffy.enable_rounding();
} else {
self.taffy.disable_rounding();
}
let out = match self.taffy.layout(taffy_node.id).cloned() {
Ok(layout) => {
self.taffy.disable_rounding();
let taffy_size = self.taffy.layout(taffy_node.id).unwrap().size;
let unrounded_size = Vec2::new(taffy_size.width, taffy_size.height);
Ok((layout, unrounded_size))
}
Err(taffy_error) => Err(LayoutError::TaffyError(taffy_error)),
};
self.taffy.enable_rounding();
out
}
}
pub fn get_text_buffer<'a>(
needs_buffer: bool,
ctx: &mut NodeMeasure,
query: &'a mut bevy_ecs::prelude::Query<&mut bevy_text::ComputedTextBlock>,
) -> Option<&'a mut bevy_text::ComputedTextBlock> {
// We avoid a query lookup whenever the buffer is not required.
if !needs_buffer {
return None;
}
let NodeMeasure::Text(crate::widget::TextMeasure { info }) = ctx else {
return None;
};
let Ok(computed) = query.get_mut(info.entity) else {
return None;
};
Some(computed.into_inner())
}
#[cfg(test)]
mod tests {
use super::*;
use crate::{ContentSize, FixedMeasure};
use bevy_math::Vec2;
use taffy::TraversePartialTree;
#[test]
fn test_initialization() {
let ui_surface = UiSurface::default();
assert!(ui_surface.entity_to_taffy.is_empty());
assert_eq!(ui_surface.taffy.total_node_count(), 0);
}
#[test]
fn test_upsert() {
let mut ui_surface = UiSurface::default();
let root_node_entity = Entity::from_raw(1);
let node = Node::default();
// standard upsert
ui_surface.upsert_node(&LayoutContext::TEST_CONTEXT, root_node_entity, &node, None);
// should be inserted into taffy
assert_eq!(ui_surface.taffy.total_node_count(), 1);
assert!(ui_surface.entity_to_taffy.contains_key(&root_node_entity));
// test duplicate insert 1
ui_surface.upsert_node(&LayoutContext::TEST_CONTEXT, root_node_entity, &node, None);
// node count should not have increased
assert_eq!(ui_surface.taffy.total_node_count(), 1);
// assign root node to camera
ui_surface.get_or_insert_taffy_viewport_node(root_node_entity);
// each root node will create 2 taffy nodes
assert_eq!(ui_surface.taffy.total_node_count(), 2);
// test duplicate insert 2
ui_surface.upsert_node(&LayoutContext::TEST_CONTEXT, root_node_entity, &node, None);
// node count should not have increased
assert_eq!(ui_surface.taffy.total_node_count(), 2);
}
#[test]
fn test_remove_entities() {
let mut ui_surface = UiSurface::default();
let root_node_entity = Entity::from_raw(1);
let node = Node::default();
ui_surface.upsert_node(&LayoutContext::TEST_CONTEXT, root_node_entity, &node, None);
ui_surface.get_or_insert_taffy_viewport_node(root_node_entity);
assert!(ui_surface.entity_to_taffy.contains_key(&root_node_entity));
ui_surface.remove_entities([root_node_entity]);
assert!(!ui_surface.entity_to_taffy.contains_key(&root_node_entity));
}
#[test]
fn test_try_update_measure() {
let mut ui_surface = UiSurface::default();
let root_node_entity = Entity::from_raw(1);
let node = Node::default();
ui_surface.upsert_node(&LayoutContext::TEST_CONTEXT, root_node_entity, &node, None);
let mut content_size = ContentSize::default();
content_size.set(NodeMeasure::Fixed(FixedMeasure { size: Vec2::ONE }));
let measure_func = content_size.measure.take().unwrap();
assert!(ui_surface
.update_node_context(root_node_entity, measure_func)
.is_some());
}
#[test]
fn test_update_children() {
let mut ui_surface = UiSurface::default();
let root_node_entity = Entity::from_raw(1);
let child_entity = Entity::from_raw(2);
let node = Node::default();
ui_surface.upsert_node(&LayoutContext::TEST_CONTEXT, root_node_entity, &node, None);
ui_surface.upsert_node(&LayoutContext::TEST_CONTEXT, child_entity, &node, None);
ui_surface.update_children(root_node_entity, vec![child_entity].into_iter());
let parent_node = *ui_surface.entity_to_taffy.get(&root_node_entity).unwrap();
let child_node = *ui_surface.entity_to_taffy.get(&child_entity).unwrap();
assert_eq!(ui_surface.taffy.parent(child_node.id), Some(parent_node.id));
}
#[expect(
unreachable_code,
reason = "Certain pieces of code tested here cause the test to fail if made reachable; see #16362 for progress on fixing this"
)]
#[test]
fn test_set_camera_children() {
let mut ui_surface = UiSurface::default();
let root_node_entity = Entity::from_raw(1);
let child_entity = Entity::from_raw(2);
let node = Node::default();
ui_surface.upsert_node(&LayoutContext::TEST_CONTEXT, root_node_entity, &node, None);
ui_surface.upsert_node(&LayoutContext::TEST_CONTEXT, child_entity, &node, None);
let root_taffy_node = *ui_surface.entity_to_taffy.get(&root_node_entity).unwrap();
let child_taffy = *ui_surface.entity_to_taffy.get(&child_entity).unwrap();
// set up the relationship manually
ui_surface
.taffy
.add_child(root_taffy_node.id, child_taffy.id)
.unwrap();
ui_surface.get_or_insert_taffy_viewport_node(root_node_entity);
assert_eq!(
ui_surface.taffy.parent(child_taffy.id),
Some(root_taffy_node.id)
);
let root_taffy_children = ui_surface.taffy.children(root_taffy_node.id).unwrap();
assert!(
root_taffy_children.contains(&child_taffy.id),
"root node is not a parent of child node"
);
assert_eq!(
ui_surface.taffy.child_count(root_taffy_node.id),
1,
"expected root node child count to be 1"
);
// clear camera's root nodes
ui_surface.get_or_insert_taffy_viewport_node(root_node_entity);
return; // TODO: can't pass the test if we continue - not implemented (remove allow(unreachable_code))
let root_taffy_children = ui_surface.taffy.children(root_taffy_node.id).unwrap();
assert!(
root_taffy_children.contains(&child_taffy.id),
"root node is not a parent of child node"
);
assert_eq!(
ui_surface.taffy.child_count(root_taffy_node.id),
1,
"expected root node child count to be 1"
);
// re-associate root node with viewport node
ui_surface.get_or_insert_taffy_viewport_node(root_node_entity);
let child_taffy = ui_surface.entity_to_taffy.get(&child_entity).unwrap();
let root_taffy_children = ui_surface.taffy.children(root_taffy_node.id).unwrap();
assert!(
root_taffy_children.contains(&child_taffy.id),
"root node is not a parent of child node"
);
assert_eq!(
ui_surface.taffy.child_count(root_taffy_node.id),
1,
"expected root node child count to be 1"
);
}
}

296
vendor/bevy_ui/src/lib.rs vendored Normal file
View File

@@ -0,0 +1,296 @@
#![expect(missing_docs, reason = "Not all docs are written yet, see #3492.")]
#![cfg_attr(docsrs, feature(doc_auto_cfg))]
#![doc(
html_logo_url = "https://bevyengine.org/assets/icon.png",
html_favicon_url = "https://bevyengine.org/assets/icon.png"
)]
//! This crate contains Bevy's UI system, which can be used to create UI for both 2D and 3D games
//! # Basic usage
//! Spawn UI elements with [`widget::Button`], [`ImageNode`], [`Text`](prelude::Text) and [`Node`]
//! This UI is laid out with the Flexbox and CSS Grid layout models (see <https://cssreference.io/flexbox/>)
pub mod measurement;
pub mod ui_material;
pub mod update;
pub mod widget;
#[cfg(feature = "bevy_ui_picking_backend")]
pub mod picking_backend;
use bevy_derive::{Deref, DerefMut};
use bevy_reflect::{std_traits::ReflectDefault, Reflect};
mod accessibility;
// This module is not re-exported, but is instead made public.
// This is intended to discourage accidental use of the experimental API.
pub mod experimental;
mod focus;
mod geometry;
mod layout;
mod render;
mod stack;
mod ui_node;
pub use focus::*;
pub use geometry::*;
pub use layout::*;
pub use measurement::*;
pub use render::*;
pub use ui_material::*;
pub use ui_node::*;
use widget::{ImageNode, ImageNodeSize};
/// The UI prelude.
///
/// This includes the most common types in this crate, re-exported for your convenience.
pub mod prelude {
#[cfg(feature = "bevy_ui_picking_backend")]
#[doc(hidden)]
pub use crate::picking_backend::{UiPickingCamera, UiPickingPlugin, UiPickingSettings};
#[doc(hidden)]
#[cfg(feature = "bevy_ui_debug")]
pub use crate::render::UiDebugOptions;
#[doc(hidden)]
pub use crate::widget::{Text, TextUiReader, TextUiWriter};
#[doc(hidden)]
pub use {
crate::{
geometry::*,
ui_material::*,
ui_node::*,
widget::{Button, ImageNode, Label, NodeImageMode},
Interaction, MaterialNode, UiMaterialPlugin, UiScale,
},
// `bevy_sprite` re-exports for texture slicing
bevy_sprite::{BorderRect, SliceScaleMode, SpriteImageMode, TextureSlicer},
};
}
use bevy_app::{prelude::*, Animation};
use bevy_ecs::prelude::*;
use bevy_input::InputSystem;
use bevy_render::{camera::CameraUpdateSystem, RenderApp};
use bevy_transform::TransformSystem;
use layout::ui_surface::UiSurface;
use stack::ui_stack_system;
pub use stack::UiStack;
use update::{update_clipping_system, update_ui_context_system};
/// The basic plugin for Bevy UI
pub struct UiPlugin {
/// If set to false, the UI's rendering systems won't be added to the `RenderApp` and no UI elements will be drawn.
/// The layout and interaction components will still be updated as normal.
pub enable_rendering: bool,
}
impl Default for UiPlugin {
fn default() -> Self {
Self {
enable_rendering: true,
}
}
}
/// The label enum labeling the types of systems in the Bevy UI
#[derive(Debug, Hash, PartialEq, Eq, Clone, SystemSet)]
pub enum UiSystem {
/// After this label, input interactions with UI entities have been updated for this frame.
///
/// Runs in [`PreUpdate`].
Focus,
/// All UI systems in [`PostUpdate`] will run in or after this label.
Prepare,
/// Update content requirements before layout.
Content,
/// After this label, the ui layout state has been updated.
///
/// Runs in [`PostUpdate`].
Layout,
/// UI systems ordered after [`UiSystem::Layout`].
///
/// Runs in [`PostUpdate`].
PostLayout,
/// After this label, the [`UiStack`] resource has been updated.
///
/// Runs in [`PostUpdate`].
Stack,
}
/// The current scale of the UI.
///
/// A multiplier to fixed-sized ui values.
/// **Note:** This will only affect fixed ui values like [`Val::Px`]
#[derive(Debug, Reflect, Resource, Deref, DerefMut)]
#[reflect(Resource, Debug, Default)]
pub struct UiScale(pub f32);
impl Default for UiScale {
fn default() -> Self {
Self(1.0)
}
}
// Marks systems that can be ambiguous with [`widget::text_system`] if the `bevy_text` feature is enabled.
// See https://github.com/bevyengine/bevy/pull/11391 for more details.
#[derive(SystemSet, Debug, Hash, PartialEq, Eq, Clone)]
struct AmbiguousWithTextSystem;
#[derive(SystemSet, Debug, Hash, PartialEq, Eq, Clone)]
struct AmbiguousWithUpdateText2DLayout;
impl Plugin for UiPlugin {
fn build(&self, app: &mut App) {
app.init_resource::<UiSurface>()
.init_resource::<UiScale>()
.init_resource::<UiStack>()
.register_type::<BackgroundColor>()
.register_type::<CalculatedClip>()
.register_type::<ComputedNode>()
.register_type::<ContentSize>()
.register_type::<FocusPolicy>()
.register_type::<Interaction>()
.register_type::<Node>()
.register_type::<RelativeCursorPosition>()
.register_type::<ScrollPosition>()
.register_type::<UiTargetCamera>()
.register_type::<ImageNode>()
.register_type::<ImageNodeSize>()
.register_type::<UiRect>()
.register_type::<UiScale>()
.register_type::<BorderColor>()
.register_type::<BorderRadius>()
.register_type::<BoxShadow>()
.register_type::<widget::Button>()
.register_type::<widget::Label>()
.register_type::<ZIndex>()
.register_type::<Outline>()
.register_type::<BoxShadowSamples>()
.register_type::<UiAntiAlias>()
.register_type::<TextShadow>()
.register_type::<ComputedNodeTarget>()
.configure_sets(
PostUpdate,
(
CameraUpdateSystem,
UiSystem::Prepare.after(Animation),
UiSystem::Content,
UiSystem::Layout,
UiSystem::PostLayout,
)
.chain(),
)
.add_systems(
PreUpdate,
ui_focus_system.in_set(UiSystem::Focus).after(InputSystem),
);
#[cfg(feature = "bevy_ui_picking_backend")]
app.add_plugins(picking_backend::UiPickingPlugin);
let ui_layout_system_config = ui_layout_system
.in_set(UiSystem::Layout)
.before(TransformSystem::TransformPropagate);
let ui_layout_system_config = ui_layout_system_config
// Text and Text2D operate on disjoint sets of entities
.ambiguous_with(bevy_text::update_text2d_layout)
.ambiguous_with(bevy_text::detect_text_needs_rerender::<bevy_text::Text2d>);
app.add_systems(
PostUpdate,
(
update_ui_context_system.in_set(UiSystem::Prepare),
ui_layout_system_config,
ui_stack_system
.in_set(UiSystem::Stack)
// the systems don't care about stack index
.ambiguous_with(update_clipping_system)
.ambiguous_with(ui_layout_system)
.in_set(AmbiguousWithTextSystem),
update_clipping_system.after(TransformSystem::TransformPropagate),
// Potential conflicts: `Assets<Image>`
// They run independently since `widget::image_node_system` will only ever observe
// its own ImageNode, and `widget::text_system` & `bevy_text::update_text2d_layout`
// will never modify a pre-existing `Image` asset.
widget::update_image_content_size_system
.in_set(UiSystem::Content)
.in_set(AmbiguousWithTextSystem)
.in_set(AmbiguousWithUpdateText2DLayout),
),
);
build_text_interop(app);
if !self.enable_rendering {
return;
}
#[cfg(feature = "bevy_ui_debug")]
app.init_resource::<UiDebugOptions>();
build_ui_render(app);
}
fn finish(&self, app: &mut App) {
if !self.enable_rendering {
return;
}
let Some(render_app) = app.get_sub_app_mut(RenderApp) else {
return;
};
render_app.init_resource::<UiPipeline>();
}
}
fn build_text_interop(app: &mut App) {
use crate::widget::TextNodeFlags;
use bevy_text::TextLayoutInfo;
use widget::Text;
app.register_type::<TextLayoutInfo>()
.register_type::<TextNodeFlags>()
.register_type::<Text>();
app.add_systems(
PostUpdate,
(
(
bevy_text::detect_text_needs_rerender::<Text>,
widget::measure_text_system,
)
.chain()
.in_set(UiSystem::Content)
// Text and Text2d are independent.
.ambiguous_with(bevy_text::detect_text_needs_rerender::<bevy_text::Text2d>)
// Potential conflict: `Assets<Image>`
// Since both systems will only ever insert new [`Image`] assets,
// they will never observe each other's effects.
.ambiguous_with(bevy_text::update_text2d_layout)
// We assume Text is on disjoint UI entities to ImageNode and UiTextureAtlasImage
// FIXME: Add an archetype invariant for this https://github.com/bevyengine/bevy/issues/1481.
.ambiguous_with(widget::update_image_content_size_system),
widget::text_system
.in_set(UiSystem::PostLayout)
.after(bevy_text::remove_dropped_font_atlas_sets)
.before(bevy_asset::AssetEvents)
// Text2d and bevy_ui text are entirely on separate entities
.ambiguous_with(bevy_text::detect_text_needs_rerender::<bevy_text::Text2d>)
.ambiguous_with(bevy_text::update_text2d_layout)
.ambiguous_with(bevy_text::calculate_bounds_text2d),
),
);
app.add_plugins(accessibility::AccessibilityPlugin);
app.configure_sets(
PostUpdate,
AmbiguousWithTextSystem.ambiguous_with(widget::text_system),
);
app.configure_sets(
PostUpdate,
AmbiguousWithUpdateText2DLayout.ambiguous_with(bevy_text::update_text2d_layout),
);
}

93
vendor/bevy_ui/src/measurement.rs vendored Normal file
View File

@@ -0,0 +1,93 @@
use bevy_ecs::{prelude::Component, reflect::ReflectComponent};
use bevy_math::Vec2;
use bevy_reflect::{std_traits::ReflectDefault, Reflect};
use bevy_text::CosmicFontSystem;
use core::fmt::Formatter;
pub use taffy::style::AvailableSpace;
use crate::widget::ImageMeasure;
use crate::widget::TextMeasure;
impl core::fmt::Debug for ContentSize {
fn fmt(&self, f: &mut Formatter<'_>) -> core::fmt::Result {
f.debug_struct("ContentSize").finish()
}
}
pub struct MeasureArgs<'a> {
pub width: Option<f32>,
pub height: Option<f32>,
pub available_width: AvailableSpace,
pub available_height: AvailableSpace,
pub font_system: &'a mut CosmicFontSystem,
pub buffer: Option<&'a mut bevy_text::ComputedTextBlock>,
}
/// A `Measure` is used to compute the size of a ui node
/// when the size of that node is based on its content.
pub trait Measure: Send + Sync + 'static {
/// Calculate the size of the node given the constraints.
fn measure(&mut self, measure_args: MeasureArgs<'_>, style: &taffy::Style) -> Vec2;
}
/// A type to serve as Taffy's node context (which allows the content size of leaf nodes to be computed)
///
/// It has specific variants for common built-in types to avoid making them opaque and needing to box them
/// by wrapping them in a closure and a Custom variant that allows arbitrary measurement closures if required.
pub enum NodeMeasure {
Fixed(FixedMeasure),
Text(TextMeasure),
Image(ImageMeasure),
Custom(Box<dyn Measure>),
}
impl Measure for NodeMeasure {
fn measure(&mut self, measure_args: MeasureArgs, style: &taffy::Style) -> Vec2 {
match self {
NodeMeasure::Fixed(fixed) => fixed.measure(measure_args, style),
NodeMeasure::Text(text) => text.measure(measure_args, style),
NodeMeasure::Image(image) => image.measure(measure_args, style),
NodeMeasure::Custom(custom) => custom.measure(measure_args, style),
}
}
}
/// A `FixedMeasure` is a `Measure` that ignores all constraints and
/// always returns the same size.
#[derive(Default, Clone)]
pub struct FixedMeasure {
pub size: Vec2,
}
impl Measure for FixedMeasure {
fn measure(&mut self, _: MeasureArgs, _: &taffy::Style) -> Vec2 {
self.size
}
}
/// A node with a `ContentSize` component is a node where its size
/// is based on its content.
#[derive(Component, Reflect, Default)]
#[reflect(Component, Default)]
pub struct ContentSize {
/// The `Measure` used to compute the intrinsic size
#[reflect(ignore)]
pub(crate) measure: Option<NodeMeasure>,
}
impl ContentSize {
/// Set a `Measure` for the UI node entity with this component
pub fn set(&mut self, measure: NodeMeasure) {
self.measure = Some(measure);
}
/// Creates a `ContentSize` with a `Measure` that always returns given `size` argument, regardless of the UI layout's constraints.
pub fn fixed_size(size: Vec2) -> ContentSize {
let mut content_size = Self::default();
content_size.set(NodeMeasure::Fixed(FixedMeasure { size }));
content_size
}
}

269
vendor/bevy_ui/src/picking_backend.rs vendored Normal file
View File

@@ -0,0 +1,269 @@
//! A picking backend for UI nodes.
//!
//! # Usage
//!
//! This backend does not require markers on cameras or entities to function. It will look for any
//! pointers using the same render target as the UI camera, and run hit tests on the UI node tree.
//!
//! ## Important Note
//!
//! This backend completely ignores [`FocusPolicy`](crate::FocusPolicy). The design of `bevy_ui`'s
//! focus systems and the picking plugin are not compatible. Instead, use the optional [`Pickable`] component
//! to override how an entity responds to picking focus. Nodes without the [`Pickable`] component
//! will still trigger events and block items below it from being hovered.
//!
//! ## Implementation Notes
//!
//! - `bevy_ui` can only render to the primary window
//! - `bevy_ui` can render on any camera with a flag, it is special, and is not tied to a particular
//! camera.
//! - To correctly sort picks, the order of `bevy_ui` is set to be the camera order plus 0.5.
//! - The `position` reported in `HitData` is normalized relative to the node, with `(0.,0.,0.)` at
//! the top left and `(1., 1., 0.)` in the bottom right. Coordinates are relative to the entire
//! node, not just the visible region. This backend does not provide a `normal`.
#![deny(missing_docs)]
use crate::{focus::pick_rounded_rect, prelude::*, UiStack};
use bevy_app::prelude::*;
use bevy_ecs::{prelude::*, query::QueryData};
use bevy_math::{Rect, Vec2};
use bevy_platform::collections::HashMap;
use bevy_reflect::{std_traits::ReflectDefault, Reflect};
use bevy_render::prelude::*;
use bevy_transform::prelude::*;
use bevy_window::PrimaryWindow;
use bevy_picking::backend::prelude::*;
/// An optional component that marks cameras that should be used in the [`UiPickingPlugin`].
///
/// Only needed if [`UiPickingSettings::require_markers`] is set to `true`, and ignored
/// otherwise.
#[derive(Debug, Clone, Default, Component, Reflect)]
#[reflect(Debug, Default, Component)]
pub struct UiPickingCamera;
/// Runtime settings for the [`UiPickingPlugin`].
#[derive(Resource, Reflect)]
#[reflect(Resource, Default)]
pub struct UiPickingSettings {
/// When set to `true` UI picking will only consider cameras marked with
/// [`UiPickingCamera`] and entities marked with [`Pickable`]. `false` by default.
///
/// This setting is provided to give you fine-grained control over which cameras and entities
/// should be used by the UI picking backend at runtime.
pub require_markers: bool,
}
#[expect(
clippy::allow_attributes,
reason = "clippy::derivable_impls is not always linted"
)]
#[allow(
clippy::derivable_impls,
reason = "Known false positive with clippy: <https://github.com/rust-lang/rust-clippy/issues/13160>"
)]
impl Default for UiPickingSettings {
fn default() -> Self {
Self {
require_markers: false,
}
}
}
/// A plugin that adds picking support for UI nodes.
///
/// This is included by default in [`UiPlugin`](crate::UiPlugin).
#[derive(Clone)]
pub struct UiPickingPlugin;
impl Plugin for UiPickingPlugin {
fn build(&self, app: &mut App) {
app.init_resource::<UiPickingSettings>()
.register_type::<(UiPickingCamera, UiPickingSettings)>()
.add_systems(PreUpdate, ui_picking.in_set(PickSet::Backend));
}
}
/// Main query from bevy's `ui_focus_system`
#[derive(QueryData)]
#[query_data(mutable)]
pub struct NodeQuery {
entity: Entity,
node: &'static ComputedNode,
global_transform: &'static GlobalTransform,
pickable: Option<&'static Pickable>,
calculated_clip: Option<&'static CalculatedClip>,
inherited_visibility: Option<&'static InheritedVisibility>,
target_camera: &'static ComputedNodeTarget,
}
/// Computes the UI node entities under each pointer.
///
/// Bevy's [`UiStack`] orders all nodes in the order they will be rendered, which is the same order
/// we need for determining picking.
pub fn ui_picking(
pointers: Query<(&PointerId, &PointerLocation)>,
camera_query: Query<(
Entity,
&Camera,
Has<IsDefaultUiCamera>,
Has<UiPickingCamera>,
)>,
primary_window: Query<Entity, With<PrimaryWindow>>,
settings: Res<UiPickingSettings>,
ui_stack: Res<UiStack>,
node_query: Query<NodeQuery>,
mut output: EventWriter<PointerHits>,
) {
// For each camera, the pointer and its position
let mut pointer_pos_by_camera = HashMap::<Entity, HashMap<PointerId, Vec2>>::default();
for (pointer_id, pointer_location) in
pointers.iter().filter_map(|(pointer, pointer_location)| {
Some(*pointer).zip(pointer_location.location().cloned())
})
{
// This pointer is associated with a render target, which could be used by multiple
// cameras. We want to ensure we return all cameras with a matching target.
for camera in camera_query
.iter()
.filter(|(_, _, _, cam_can_pick)| !settings.require_markers || *cam_can_pick)
.map(|(entity, camera, _, _)| {
(
entity,
camera.target.normalize(primary_window.single().ok()),
)
})
.filter_map(|(entity, target)| Some(entity).zip(target))
.filter(|(_entity, target)| target == &pointer_location.target)
.map(|(cam_entity, _target)| cam_entity)
{
let Ok((_, camera_data, _, _)) = camera_query.get(camera) else {
continue;
};
let mut pointer_pos =
pointer_location.position * camera_data.target_scaling_factor().unwrap_or(1.);
if let Some(viewport) = camera_data.physical_viewport_rect() {
pointer_pos -= viewport.min.as_vec2();
}
pointer_pos_by_camera
.entry(camera)
.or_default()
.insert(pointer_id, pointer_pos);
}
}
// The list of node entities hovered for each (camera, pointer) combo
let mut hit_nodes = HashMap::<(Entity, PointerId), Vec<(Entity, Vec2)>>::default();
// prepare an iterator that contains all the nodes that have the cursor in their rect,
// from the top node to the bottom one. this will also reset the interaction to `None`
// for all nodes encountered that are no longer hovered.
for node_entity in ui_stack
.uinodes
.iter()
// reverse the iterator to traverse the tree from closest nodes to furthest
.rev()
{
let Ok(node) = node_query.get(*node_entity) else {
continue;
};
if settings.require_markers && node.pickable.is_none() {
continue;
}
// Nodes that are not rendered should not be interactable
if node
.inherited_visibility
.map(|inherited_visibility| inherited_visibility.get())
!= Some(true)
{
continue;
}
let Some(camera_entity) = node.target_camera.camera() else {
continue;
};
let node_rect = Rect::from_center_size(
node.global_transform.translation().truncate(),
node.node.size(),
);
// Nodes with Display::None have a (0., 0.) logical rect and can be ignored
if node_rect.size() == Vec2::ZERO {
continue;
}
// Intersect with the calculated clip rect to find the bounds of the visible region of the node
let visible_rect = node
.calculated_clip
.map(|clip| node_rect.intersect(clip.clip))
.unwrap_or(node_rect);
let pointers_on_this_cam = pointer_pos_by_camera.get(&camera_entity);
// The mouse position relative to the node
// (0., 0.) is the top-left corner, (1., 1.) is the bottom-right corner
// Coordinates are relative to the entire node, not just the visible region.
for (pointer_id, cursor_position) in pointers_on_this_cam.iter().flat_map(|h| h.iter()) {
let relative_cursor_position = (*cursor_position - node_rect.min) / node_rect.size();
if visible_rect
.normalize(node_rect)
.contains(relative_cursor_position)
&& pick_rounded_rect(
*cursor_position - node_rect.center(),
node_rect.size(),
node.node.border_radius,
)
{
hit_nodes
.entry((camera_entity, *pointer_id))
.or_default()
.push((*node_entity, relative_cursor_position));
}
}
}
for ((camera, pointer), hovered) in hit_nodes.iter() {
// As soon as a node with a `Block` focus policy is detected, the iteration will stop on it
// because it "captures" the interaction.
let mut picks = Vec::new();
let mut depth = 0.0;
for (hovered_node, position) in hovered {
let node = node_query.get(*hovered_node).unwrap();
let Some(camera_entity) = node.target_camera.camera() else {
continue;
};
picks.push((
node.entity,
HitData::new(camera_entity, depth, Some(position.extend(0.0)), None),
));
if let Some(pickable) = node.pickable {
// If an entity has a `Pickable` component, we will use that as the source of truth.
if pickable.should_block_lower {
break;
}
} else {
// If the `Pickable` component doesn't exist, default behavior is to block.
break;
}
depth += 0.00001; // keep depth near 0 for precision
}
let order = camera_query
.get(*camera)
.map(|(_, cam, _, _)| cam.order)
.unwrap_or_default() as f32
+ 0.5; // bevy ui can run on any camera, it's a special case
output.write(PointerHits::new(*pointer, picks, order));
}
}

588
vendor/bevy_ui/src/render/box_shadow.rs vendored Normal file
View File

@@ -0,0 +1,588 @@
//! Box shadows rendering
use core::{hash::Hash, ops::Range};
use crate::{
BoxShadow, BoxShadowSamples, CalculatedClip, ComputedNode, ComputedNodeTarget, RenderUiSystem,
ResolvedBorderRadius, TransparentUi, Val,
};
use bevy_app::prelude::*;
use bevy_asset::*;
use bevy_color::{Alpha, ColorToComponents, LinearRgba};
use bevy_ecs::prelude::*;
use bevy_ecs::{
prelude::Component,
system::{
lifetimeless::{Read, SRes},
*,
},
};
use bevy_image::BevyDefault as _;
use bevy_math::{vec2, FloatOrd, Mat4, Rect, Vec2, Vec3Swizzles, Vec4Swizzles};
use bevy_render::sync_world::MainEntity;
use bevy_render::RenderApp;
use bevy_render::{
render_phase::*,
render_resource::{binding_types::uniform_buffer, *},
renderer::{RenderDevice, RenderQueue},
sync_world::TemporaryRenderEntity,
view::*,
Extract, ExtractSchedule, Render, RenderSet,
};
use bevy_transform::prelude::GlobalTransform;
use bytemuck::{Pod, Zeroable};
use super::{stack_z_offsets, UiCameraMap, UiCameraView, QUAD_INDICES, QUAD_VERTEX_POSITIONS};
pub const BOX_SHADOW_SHADER_HANDLE: Handle<Shader> =
weak_handle!("d2991ecd-134f-4f82-adf5-0fcc86f02227");
/// A plugin that enables the rendering of box shadows.
pub struct BoxShadowPlugin;
impl Plugin for BoxShadowPlugin {
fn build(&self, app: &mut App) {
load_internal_asset!(
app,
BOX_SHADOW_SHADER_HANDLE,
"box_shadow.wgsl",
Shader::from_wgsl
);
if let Some(render_app) = app.get_sub_app_mut(RenderApp) {
render_app
.add_render_command::<TransparentUi, DrawBoxShadows>()
.init_resource::<ExtractedBoxShadows>()
.init_resource::<BoxShadowMeta>()
.init_resource::<SpecializedRenderPipelines<BoxShadowPipeline>>()
.add_systems(
ExtractSchedule,
extract_shadows.in_set(RenderUiSystem::ExtractBoxShadows),
)
.add_systems(
Render,
(
queue_shadows.in_set(RenderSet::Queue),
prepare_shadows.in_set(RenderSet::PrepareBindGroups),
),
);
}
}
fn finish(&self, app: &mut App) {
if let Some(render_app) = app.get_sub_app_mut(RenderApp) {
render_app.init_resource::<BoxShadowPipeline>();
}
}
}
#[repr(C)]
#[derive(Copy, Clone, Pod, Zeroable)]
struct BoxShadowVertex {
position: [f32; 3],
uvs: [f32; 2],
vertex_color: [f32; 4],
size: [f32; 2],
radius: [f32; 4],
blur: f32,
bounds: [f32; 2],
}
#[derive(Component)]
pub struct UiShadowsBatch {
pub range: Range<u32>,
pub camera: Entity,
}
/// Contains the vertices and bind groups to be sent to the GPU
#[derive(Resource)]
pub struct BoxShadowMeta {
vertices: RawBufferVec<BoxShadowVertex>,
indices: RawBufferVec<u32>,
view_bind_group: Option<BindGroup>,
}
impl Default for BoxShadowMeta {
fn default() -> Self {
Self {
vertices: RawBufferVec::new(BufferUsages::VERTEX),
indices: RawBufferVec::new(BufferUsages::INDEX),
view_bind_group: None,
}
}
}
#[derive(Resource)]
pub struct BoxShadowPipeline {
pub view_layout: BindGroupLayout,
}
impl FromWorld for BoxShadowPipeline {
fn from_world(world: &mut World) -> Self {
let render_device = world.resource::<RenderDevice>();
let view_layout = render_device.create_bind_group_layout(
"box_shadow_view_layout",
&BindGroupLayoutEntries::single(
ShaderStages::VERTEX_FRAGMENT,
uniform_buffer::<ViewUniform>(true),
),
);
BoxShadowPipeline { view_layout }
}
}
#[derive(Clone, Copy, Hash, PartialEq, Eq)]
pub struct BoxShadowPipelineKey {
pub hdr: bool,
/// Number of samples, a higher value results in better quality shadows.
pub samples: u32,
}
impl SpecializedRenderPipeline for BoxShadowPipeline {
type Key = BoxShadowPipelineKey;
fn specialize(&self, key: Self::Key) -> RenderPipelineDescriptor {
let vertex_layout = VertexBufferLayout::from_vertex_formats(
VertexStepMode::Vertex,
vec![
// position
VertexFormat::Float32x3,
// uv
VertexFormat::Float32x2,
// color
VertexFormat::Float32x4,
// target rect size
VertexFormat::Float32x2,
// corner radius values (top left, top right, bottom right, bottom left)
VertexFormat::Float32x4,
// blur radius
VertexFormat::Float32,
// outer size
VertexFormat::Float32x2,
],
);
let shader_defs = vec![ShaderDefVal::UInt(
"SHADOW_SAMPLES".to_string(),
key.samples,
)];
RenderPipelineDescriptor {
vertex: VertexState {
shader: BOX_SHADOW_SHADER_HANDLE,
entry_point: "vertex".into(),
shader_defs: shader_defs.clone(),
buffers: vec![vertex_layout],
},
fragment: Some(FragmentState {
shader: BOX_SHADOW_SHADER_HANDLE,
shader_defs,
entry_point: "fragment".into(),
targets: vec![Some(ColorTargetState {
format: if key.hdr {
ViewTarget::TEXTURE_FORMAT_HDR
} else {
TextureFormat::bevy_default()
},
blend: Some(BlendState::ALPHA_BLENDING),
write_mask: ColorWrites::ALL,
})],
}),
layout: vec![self.view_layout.clone()],
push_constant_ranges: Vec::new(),
primitive: PrimitiveState {
front_face: FrontFace::Ccw,
cull_mode: None,
unclipped_depth: false,
polygon_mode: PolygonMode::Fill,
conservative: false,
topology: PrimitiveTopology::TriangleList,
strip_index_format: None,
},
depth_stencil: None,
multisample: MultisampleState {
count: 1,
mask: !0,
alpha_to_coverage_enabled: false,
},
label: Some("box_shadow_pipeline".into()),
zero_initialize_workgroup_memory: false,
}
}
}
/// Description of a shadow to be sorted and queued for rendering
pub struct ExtractedBoxShadow {
pub stack_index: u32,
pub transform: Mat4,
pub bounds: Vec2,
pub clip: Option<Rect>,
pub extracted_camera_entity: Entity,
pub color: LinearRgba,
pub radius: ResolvedBorderRadius,
pub blur_radius: f32,
pub size: Vec2,
pub main_entity: MainEntity,
pub render_entity: Entity,
}
/// List of extracted shadows to be sorted and queued for rendering
#[derive(Resource, Default)]
pub struct ExtractedBoxShadows {
pub box_shadows: Vec<ExtractedBoxShadow>,
}
pub fn extract_shadows(
mut commands: Commands,
mut extracted_box_shadows: ResMut<ExtractedBoxShadows>,
box_shadow_query: Extract<
Query<(
Entity,
&ComputedNode,
&GlobalTransform,
&InheritedVisibility,
&BoxShadow,
Option<&CalculatedClip>,
&ComputedNodeTarget,
)>,
>,
camera_map: Extract<UiCameraMap>,
) {
let mut mapping = camera_map.get_mapper();
for (entity, uinode, transform, visibility, box_shadow, clip, camera) in &box_shadow_query {
// Skip if no visible shadows
if !visibility.get() || box_shadow.is_empty() || uinode.is_empty() {
continue;
}
let Some(extracted_camera_entity) = mapping.map(camera) else {
continue;
};
let ui_physical_viewport_size = camera.physical_size.as_vec2();
let scale_factor = uinode.inverse_scale_factor.recip();
for drop_shadow in box_shadow.iter() {
if drop_shadow.color.is_fully_transparent() {
continue;
}
let resolve_val = |val, base, scale_factor| match val {
Val::Auto => 0.,
Val::Px(px) => px * scale_factor,
Val::Percent(percent) => percent / 100. * base,
Val::Vw(percent) => percent / 100. * ui_physical_viewport_size.x,
Val::Vh(percent) => percent / 100. * ui_physical_viewport_size.y,
Val::VMin(percent) => percent / 100. * ui_physical_viewport_size.min_element(),
Val::VMax(percent) => percent / 100. * ui_physical_viewport_size.max_element(),
};
let spread_x = resolve_val(drop_shadow.spread_radius, uinode.size().x, scale_factor);
let spread_ratio = (spread_x + uinode.size().x) / uinode.size().x;
let spread = vec2(spread_x, uinode.size().y * spread_ratio - uinode.size().y);
let blur_radius = resolve_val(drop_shadow.blur_radius, uinode.size().x, scale_factor);
let offset = vec2(
resolve_val(drop_shadow.x_offset, uinode.size().x, scale_factor),
resolve_val(drop_shadow.y_offset, uinode.size().y, scale_factor),
);
let shadow_size = uinode.size() + spread;
if shadow_size.cmple(Vec2::ZERO).any() {
continue;
}
let radius = ResolvedBorderRadius {
top_left: uinode.border_radius.top_left * spread_ratio,
top_right: uinode.border_radius.top_right * spread_ratio,
bottom_left: uinode.border_radius.bottom_left * spread_ratio,
bottom_right: uinode.border_radius.bottom_right * spread_ratio,
};
extracted_box_shadows.box_shadows.push(ExtractedBoxShadow {
render_entity: commands.spawn(TemporaryRenderEntity).id(),
stack_index: uinode.stack_index,
transform: transform.compute_matrix() * Mat4::from_translation(offset.extend(0.)),
color: drop_shadow.color.into(),
bounds: shadow_size + 6. * blur_radius,
clip: clip.map(|clip| clip.clip),
extracted_camera_entity,
radius,
blur_radius,
size: shadow_size,
main_entity: entity.into(),
});
}
}
}
#[expect(
clippy::too_many_arguments,
reason = "it's a system that needs a lot of them"
)]
pub fn queue_shadows(
extracted_box_shadows: ResMut<ExtractedBoxShadows>,
box_shadow_pipeline: Res<BoxShadowPipeline>,
mut pipelines: ResMut<SpecializedRenderPipelines<BoxShadowPipeline>>,
mut transparent_render_phases: ResMut<ViewSortedRenderPhases<TransparentUi>>,
mut render_views: Query<(&UiCameraView, Option<&BoxShadowSamples>), With<ExtractedView>>,
camera_views: Query<&ExtractedView>,
pipeline_cache: Res<PipelineCache>,
draw_functions: Res<DrawFunctions<TransparentUi>>,
) {
let draw_function = draw_functions.read().id::<DrawBoxShadows>();
for (index, extracted_shadow) in extracted_box_shadows.box_shadows.iter().enumerate() {
let entity = extracted_shadow.render_entity;
let Ok((default_camera_view, shadow_samples)) =
render_views.get_mut(extracted_shadow.extracted_camera_entity)
else {
continue;
};
let Ok(view) = camera_views.get(default_camera_view.0) else {
continue;
};
let Some(transparent_phase) = transparent_render_phases.get_mut(&view.retained_view_entity)
else {
continue;
};
let pipeline = pipelines.specialize(
&pipeline_cache,
&box_shadow_pipeline,
BoxShadowPipelineKey {
hdr: view.hdr,
samples: shadow_samples.copied().unwrap_or_default().0,
},
);
transparent_phase.add(TransparentUi {
draw_function,
pipeline,
entity: (entity, extracted_shadow.main_entity),
sort_key: FloatOrd(extracted_shadow.stack_index as f32 + stack_z_offsets::BOX_SHADOW),
batch_range: 0..0,
extra_index: PhaseItemExtraIndex::None,
index,
indexed: true,
});
}
}
pub fn prepare_shadows(
mut commands: Commands,
render_device: Res<RenderDevice>,
render_queue: Res<RenderQueue>,
mut ui_meta: ResMut<BoxShadowMeta>,
mut extracted_shadows: ResMut<ExtractedBoxShadows>,
view_uniforms: Res<ViewUniforms>,
box_shadow_pipeline: Res<BoxShadowPipeline>,
mut phases: ResMut<ViewSortedRenderPhases<TransparentUi>>,
mut previous_len: Local<usize>,
) {
if let Some(view_binding) = view_uniforms.uniforms.binding() {
let mut batches: Vec<(Entity, UiShadowsBatch)> = Vec::with_capacity(*previous_len);
ui_meta.vertices.clear();
ui_meta.indices.clear();
ui_meta.view_bind_group = Some(render_device.create_bind_group(
"box_shadow_view_bind_group",
&box_shadow_pipeline.view_layout,
&BindGroupEntries::single(view_binding),
));
// Buffer indexes
let mut vertices_index = 0;
let mut indices_index = 0;
for ui_phase in phases.values_mut() {
for item_index in 0..ui_phase.items.len() {
let item = &mut ui_phase.items[item_index];
if let Some(box_shadow) = extracted_shadows
.box_shadows
.get(item.index)
.filter(|n| item.entity() == n.render_entity)
{
let rect_size = box_shadow.bounds.extend(1.0);
// Specify the corners of the node
let positions = QUAD_VERTEX_POSITIONS
.map(|pos| (box_shadow.transform * (pos * rect_size).extend(1.)).xyz());
// Calculate the effect of clipping
// Note: this won't work with rotation/scaling, but that's much more complex (may need more that 2 quads)
let positions_diff = if let Some(clip) = box_shadow.clip {
[
Vec2::new(
f32::max(clip.min.x - positions[0].x, 0.),
f32::max(clip.min.y - positions[0].y, 0.),
),
Vec2::new(
f32::min(clip.max.x - positions[1].x, 0.),
f32::max(clip.min.y - positions[1].y, 0.),
),
Vec2::new(
f32::min(clip.max.x - positions[2].x, 0.),
f32::min(clip.max.y - positions[2].y, 0.),
),
Vec2::new(
f32::max(clip.min.x - positions[3].x, 0.),
f32::min(clip.max.y - positions[3].y, 0.),
),
]
} else {
[Vec2::ZERO; 4]
};
let positions_clipped = [
positions[0] + positions_diff[0].extend(0.),
positions[1] + positions_diff[1].extend(0.),
positions[2] + positions_diff[2].extend(0.),
positions[3] + positions_diff[3].extend(0.),
];
let transformed_rect_size = box_shadow.transform.transform_vector3(rect_size);
// Don't try to cull nodes that have a rotation
// In a rotation around the Z-axis, this value is 0.0 for an angle of 0.0 or π
// In those two cases, the culling check can proceed normally as corners will be on
// horizontal / vertical lines
// For all other angles, bypass the culling check
// This does not properly handles all rotations on all axis
if box_shadow.transform.x_axis[1] == 0.0 {
// Cull nodes that are completely clipped
if positions_diff[0].x - positions_diff[1].x >= transformed_rect_size.x
|| positions_diff[1].y - positions_diff[2].y >= transformed_rect_size.y
{
continue;
}
}
let radius = [
box_shadow.radius.top_left,
box_shadow.radius.top_right,
box_shadow.radius.bottom_right,
box_shadow.radius.bottom_left,
];
let uvs = [
Vec2::new(positions_diff[0].x, positions_diff[0].y),
Vec2::new(
box_shadow.bounds.x + positions_diff[1].x,
positions_diff[1].y,
),
Vec2::new(
box_shadow.bounds.x + positions_diff[2].x,
box_shadow.bounds.y + positions_diff[2].y,
),
Vec2::new(
positions_diff[3].x,
box_shadow.bounds.y + positions_diff[3].y,
),
]
.map(|pos| pos / box_shadow.bounds);
for i in 0..4 {
ui_meta.vertices.push(BoxShadowVertex {
position: positions_clipped[i].into(),
uvs: uvs[i].into(),
vertex_color: box_shadow.color.to_f32_array(),
size: box_shadow.size.into(),
radius,
blur: box_shadow.blur_radius,
bounds: rect_size.xy().into(),
});
}
for &i in &QUAD_INDICES {
ui_meta.indices.push(indices_index + i as u32);
}
batches.push((
item.entity(),
UiShadowsBatch {
range: vertices_index..vertices_index + 6,
camera: box_shadow.extracted_camera_entity,
},
));
vertices_index += 6;
indices_index += 4;
// shadows are sent to the gpu non-batched
*ui_phase.items[item_index].batch_range_mut() =
item_index as u32..item_index as u32 + 1;
}
}
}
ui_meta.vertices.write_buffer(&render_device, &render_queue);
ui_meta.indices.write_buffer(&render_device, &render_queue);
*previous_len = batches.len();
commands.try_insert_batch(batches);
}
extracted_shadows.box_shadows.clear();
}
pub type DrawBoxShadows = (SetItemPipeline, SetBoxShadowViewBindGroup<0>, DrawBoxShadow);
pub struct SetBoxShadowViewBindGroup<const I: usize>;
impl<P: PhaseItem, const I: usize> RenderCommand<P> for SetBoxShadowViewBindGroup<I> {
type Param = SRes<BoxShadowMeta>;
type ViewQuery = Read<ViewUniformOffset>;
type ItemQuery = ();
fn render<'w>(
_item: &P,
view_uniform: &'w ViewUniformOffset,
_entity: Option<()>,
ui_meta: SystemParamItem<'w, '_, Self::Param>,
pass: &mut TrackedRenderPass<'w>,
) -> RenderCommandResult {
let Some(view_bind_group) = ui_meta.into_inner().view_bind_group.as_ref() else {
return RenderCommandResult::Failure("view_bind_group not available");
};
pass.set_bind_group(I, view_bind_group, &[view_uniform.offset]);
RenderCommandResult::Success
}
}
pub struct DrawBoxShadow;
impl<P: PhaseItem> RenderCommand<P> for DrawBoxShadow {
type Param = SRes<BoxShadowMeta>;
type ViewQuery = ();
type ItemQuery = Read<UiShadowsBatch>;
#[inline]
fn render<'w>(
_item: &P,
_view: (),
batch: Option<&'w UiShadowsBatch>,
ui_meta: SystemParamItem<'w, '_, Self::Param>,
pass: &mut TrackedRenderPass<'w>,
) -> RenderCommandResult {
let Some(batch) = batch else {
return RenderCommandResult::Skip;
};
let ui_meta = ui_meta.into_inner();
let Some(vertices) = ui_meta.vertices.buffer() else {
return RenderCommandResult::Failure("missing vertices to draw ui");
};
let Some(indices) = ui_meta.indices.buffer() else {
return RenderCommandResult::Failure("missing indices to draw ui");
};
// Store the vertices
pass.set_vertex_buffer(0, vertices.slice(..));
// Define how to "connect" the vertices
pass.set_index_buffer(indices.slice(..), 0, IndexFormat::Uint32);
// Draw the vertices
pass.draw_indexed(batch.range.clone(), 0, 0..1);
RenderCommandResult::Success
}
}

View File

@@ -0,0 +1,98 @@
#import bevy_render::view::View;
#import bevy_render::globals::Globals;
const PI: f32 = 3.14159265358979323846;
const SAMPLES: i32 = #SHADOW_SAMPLES;
@group(0) @binding(0) var<uniform> view: View;
struct BoxShadowVertexOutput {
@builtin(position) position: vec4<f32>,
@location(0) point: vec2<f32>,
@location(1) color: vec4<f32>,
@location(2) @interpolate(flat) size: vec2<f32>,
@location(3) @interpolate(flat) radius: vec4<f32>,
@location(4) @interpolate(flat) blur: f32,
}
fn gaussian(x: f32, sigma: f32) -> f32 {
return exp(-(x * x) / (2. * sigma * sigma)) / (sqrt(2. * PI) * sigma);
}
// Approximates the Gauss error function: https://en.wikipedia.org/wiki/Error_function
fn erf(p: vec2<f32>) -> vec2<f32> {
let s = sign(p);
let a = abs(p);
// fourth degree polynomial approximation for erf
var result = 1.0 + (0.278393 + (0.230389 + 0.078108 * (a * a)) * a) * a;
result = result * result;
return s - s / (result * result);
}
// returns the closest corner radius based on the signs of the components of p
fn selectCorner(p: vec2<f32>, c: vec4<f32>) -> f32 {
return mix(mix(c.x, c.y, step(0., p.x)), mix(c.w, c.z, step(0., p.x)), step(0., p.y));
}
fn horizontalRoundedBoxShadow(x: f32, y: f32, blur: f32, corner: f32, half_size: vec2<f32>) -> f32 {
let d = min(half_size.y - corner - abs(y), 0.);
let c = half_size.x - corner + sqrt(max(0., corner * corner - d * d));
let integral = 0.5 + 0.5 * erf((x + vec2(-c, c)) * (sqrt(0.5) / blur));
return integral.y - integral.x;
}
fn roundedBoxShadow(
lower: vec2<f32>,
upper: vec2<f32>,
point: vec2<f32>,
blur: f32,
corners: vec4<f32>,
) -> f32 {
let center = (lower + upper) * 0.5;
let half_size = (upper - lower) * 0.5;
let p = point - center;
let low = p.y - half_size.y;
let high = p.y + half_size.y;
let start = clamp(-3. * blur, low, high);
let end = clamp(3. * blur, low, high);
let step = (end - start) / f32(SAMPLES);
var y = start + step * 0.5;
var value: f32 = 0.0;
for (var i = 0; i < SAMPLES; i++) {
let corner = selectCorner(p, corners);
value += horizontalRoundedBoxShadow(p.x, p.y - y, blur, corner, half_size) * gaussian(y, blur) * step;
y += step;
}
return value;
}
@vertex
fn vertex(
@location(0) vertex_position: vec3<f32>,
@location(1) uv: vec2<f32>,
@location(2) vertex_color: vec4<f32>,
@location(3) size: vec2<f32>,
@location(4) radius: vec4<f32>,
@location(5) blur: f32,
@location(6) bounds: vec2<f32>,
) -> BoxShadowVertexOutput {
var out: BoxShadowVertexOutput;
out.position = view.clip_from_world * vec4(vertex_position, 1.0);
out.point = (uv.xy - 0.5) * bounds;
out.color = vertex_color;
out.size = size;
out.radius = radius;
out.blur = blur;
return out;
}
@fragment
fn fragment(
in: BoxShadowVertexOutput,
) -> @location(0) vec4<f32> {
let g = in.color.a * roundedBoxShadow(-0.5 * in.size, 0.5 * in.size, in.point, max(in.blur, 0.01), in.radius);
return vec4(in.color.rgb, g);
}

View File

@@ -0,0 +1,114 @@
use crate::ui_node::ComputedNodeTarget;
use crate::CalculatedClip;
use crate::ComputedNode;
use bevy_asset::AssetId;
use bevy_color::Hsla;
use bevy_ecs::entity::Entity;
use bevy_ecs::resource::Resource;
use bevy_ecs::system::Commands;
use bevy_ecs::system::Query;
use bevy_ecs::system::Res;
use bevy_ecs::system::ResMut;
use bevy_math::Rect;
use bevy_math::Vec2;
use bevy_render::sync_world::TemporaryRenderEntity;
use bevy_render::view::InheritedVisibility;
use bevy_render::Extract;
use bevy_sprite::BorderRect;
use bevy_transform::components::GlobalTransform;
use super::ExtractedUiItem;
use super::ExtractedUiNode;
use super::ExtractedUiNodes;
use super::NodeType;
use super::UiCameraMap;
/// Configuration for the UI debug overlay
#[derive(Resource)]
pub struct UiDebugOptions {
/// Set to true to enable the UI debug overlay
pub enabled: bool,
/// Width of the overlay's lines in logical pixels
pub line_width: f32,
/// Show outlines for non-visible UI nodes
pub show_hidden: bool,
/// Show outlines for clipped sections of UI nodes
pub show_clipped: bool,
}
impl UiDebugOptions {
pub fn toggle(&mut self) {
self.enabled = !self.enabled;
}
}
impl Default for UiDebugOptions {
fn default() -> Self {
Self {
enabled: false,
line_width: 1.,
show_hidden: false,
show_clipped: false,
}
}
}
pub fn extract_debug_overlay(
mut commands: Commands,
debug_options: Extract<Res<UiDebugOptions>>,
mut extracted_uinodes: ResMut<ExtractedUiNodes>,
uinode_query: Extract<
Query<(
Entity,
&ComputedNode,
&InheritedVisibility,
Option<&CalculatedClip>,
&GlobalTransform,
&ComputedNodeTarget,
)>,
>,
camera_map: Extract<UiCameraMap>,
) {
if !debug_options.enabled {
return;
}
let mut camera_mapper = camera_map.get_mapper();
for (entity, uinode, visibility, maybe_clip, transform, computed_target) in &uinode_query {
if !debug_options.show_hidden && !visibility.get() {
continue;
}
let Some(extracted_camera_entity) = camera_mapper.map(computed_target) else {
continue;
};
// Extract a border box to display an outline for every UI Node in the layout
extracted_uinodes.uinodes.push(ExtractedUiNode {
render_entity: commands.spawn(TemporaryRenderEntity).id(),
// Add a large number to the UI node's stack index so that the overlay is always drawn on top
stack_index: uinode.stack_index + u32::MAX / 2,
color: Hsla::sequential_dispersed(entity.index()).into(),
rect: Rect {
min: Vec2::ZERO,
max: uinode.size,
},
clip: maybe_clip
.filter(|_| !debug_options.show_clipped)
.map(|clip| clip.clip),
image: AssetId::default(),
extracted_camera_entity,
item: ExtractedUiItem::Node {
atlas_scaling: None,
transform: transform.compute_matrix(),
flip_x: false,
flip_y: false,
border: BorderRect::all(debug_options.line_width / uinode.inverse_scale_factor()),
border_radius: uinode.border_radius(),
node_type: NodeType::Border,
},
main_entity: entity.into(),
});
}
}

1386
vendor/bevy_ui/src/render/mod.rs vendored Normal file

File diff suppressed because it is too large Load Diff

127
vendor/bevy_ui/src/render/pipeline.rs vendored Normal file
View File

@@ -0,0 +1,127 @@
use bevy_ecs::prelude::*;
use bevy_image::BevyDefault as _;
use bevy_render::{
render_resource::{
binding_types::{sampler, texture_2d, uniform_buffer},
*,
},
renderer::RenderDevice,
view::{ViewTarget, ViewUniform},
};
#[derive(Resource)]
pub struct UiPipeline {
pub view_layout: BindGroupLayout,
pub image_layout: BindGroupLayout,
}
impl FromWorld for UiPipeline {
fn from_world(world: &mut World) -> Self {
let render_device = world.resource::<RenderDevice>();
let view_layout = render_device.create_bind_group_layout(
"ui_view_layout",
&BindGroupLayoutEntries::single(
ShaderStages::VERTEX_FRAGMENT,
uniform_buffer::<ViewUniform>(true),
),
);
let image_layout = render_device.create_bind_group_layout(
"ui_image_layout",
&BindGroupLayoutEntries::sequential(
ShaderStages::FRAGMENT,
(
texture_2d(TextureSampleType::Float { filterable: true }),
sampler(SamplerBindingType::Filtering),
),
),
);
UiPipeline {
view_layout,
image_layout,
}
}
}
#[derive(Clone, Copy, Hash, PartialEq, Eq)]
pub struct UiPipelineKey {
pub hdr: bool,
pub anti_alias: bool,
}
impl SpecializedRenderPipeline for UiPipeline {
type Key = UiPipelineKey;
fn specialize(&self, key: Self::Key) -> RenderPipelineDescriptor {
let vertex_layout = VertexBufferLayout::from_vertex_formats(
VertexStepMode::Vertex,
vec![
// position
VertexFormat::Float32x3,
// uv
VertexFormat::Float32x2,
// color
VertexFormat::Float32x4,
// mode
VertexFormat::Uint32,
// border radius
VertexFormat::Float32x4,
// border thickness
VertexFormat::Float32x4,
// border size
VertexFormat::Float32x2,
// position relative to the center
VertexFormat::Float32x2,
],
);
let shader_defs = if key.anti_alias {
vec!["ANTI_ALIAS".into()]
} else {
Vec::new()
};
RenderPipelineDescriptor {
vertex: VertexState {
shader: super::UI_SHADER_HANDLE,
entry_point: "vertex".into(),
shader_defs: shader_defs.clone(),
buffers: vec![vertex_layout],
},
fragment: Some(FragmentState {
shader: super::UI_SHADER_HANDLE,
shader_defs,
entry_point: "fragment".into(),
targets: vec![Some(ColorTargetState {
format: if key.hdr {
ViewTarget::TEXTURE_FORMAT_HDR
} else {
TextureFormat::bevy_default()
},
blend: Some(BlendState::ALPHA_BLENDING),
write_mask: ColorWrites::ALL,
})],
}),
layout: vec![self.view_layout.clone(), self.image_layout.clone()],
push_constant_ranges: Vec::new(),
primitive: PrimitiveState {
front_face: FrontFace::Ccw,
cull_mode: None,
unclipped_depth: false,
polygon_mode: PolygonMode::Fill,
conservative: false,
topology: PrimitiveTopology::TriangleList,
strip_index_format: None,
},
depth_stencil: None,
multisample: MultisampleState {
count: 1,
mask: !0,
alpha_to_coverage_enabled: false,
},
label: Some("ui_pipeline".into()),
zero_initialize_workgroup_memory: false,
}
}
}

269
vendor/bevy_ui/src/render/render_pass.rs vendored Normal file
View File

@@ -0,0 +1,269 @@
use core::ops::Range;
use super::{ImageNodeBindGroups, UiBatch, UiMeta, UiViewTarget};
use crate::UiCameraView;
use bevy_ecs::{
prelude::*,
system::{lifetimeless::*, SystemParamItem},
};
use bevy_math::FloatOrd;
use bevy_render::sync_world::MainEntity;
use bevy_render::{
camera::ExtractedCamera,
render_graph::*,
render_phase::*,
render_resource::{CachedRenderPipelineId, RenderPassDescriptor},
renderer::*,
view::*,
};
use tracing::error;
pub struct UiPassNode {
ui_view_query: QueryState<(&'static ExtractedView, &'static UiViewTarget)>,
ui_view_target_query: QueryState<(&'static ViewTarget, &'static ExtractedCamera)>,
ui_camera_view_query: QueryState<&'static UiCameraView>,
}
impl UiPassNode {
pub fn new(world: &mut World) -> Self {
Self {
ui_view_query: world.query_filtered(),
ui_view_target_query: world.query(),
ui_camera_view_query: world.query(),
}
}
}
impl Node for UiPassNode {
fn update(&mut self, world: &mut World) {
self.ui_view_query.update_archetypes(world);
self.ui_view_target_query.update_archetypes(world);
self.ui_camera_view_query.update_archetypes(world);
}
fn run(
&self,
graph: &mut RenderGraphContext,
render_context: &mut RenderContext,
world: &World,
) -> Result<(), NodeRunError> {
// Extract the UI view.
let input_view_entity = graph.view_entity();
let Some(transparent_render_phases) =
world.get_resource::<ViewSortedRenderPhases<TransparentUi>>()
else {
return Ok(());
};
// Query the UI view components.
let Ok((view, ui_view_target)) = self.ui_view_query.get_manual(world, input_view_entity)
else {
return Ok(());
};
let Ok((target, camera)) = self
.ui_view_target_query
.get_manual(world, ui_view_target.0)
else {
return Ok(());
};
let Some(transparent_phase) = transparent_render_phases.get(&view.retained_view_entity)
else {
return Ok(());
};
if transparent_phase.items.is_empty() {
return Ok(());
}
// use the UI view entity if it is defined
let view_entity = if let Ok(ui_camera_view) = self
.ui_camera_view_query
.get_manual(world, input_view_entity)
{
ui_camera_view.0
} else {
input_view_entity
};
let mut render_pass = render_context.begin_tracked_render_pass(RenderPassDescriptor {
label: Some("ui_pass"),
color_attachments: &[Some(target.get_unsampled_color_attachment())],
depth_stencil_attachment: None,
timestamp_writes: None,
occlusion_query_set: None,
});
if let Some(viewport) = camera.viewport.as_ref() {
render_pass.set_camera_viewport(viewport);
}
if let Err(err) = transparent_phase.render(&mut render_pass, world, view_entity) {
error!("Error encountered while rendering the ui phase {err:?}");
}
Ok(())
}
}
pub struct TransparentUi {
pub sort_key: FloatOrd,
pub entity: (Entity, MainEntity),
pub pipeline: CachedRenderPipelineId,
pub draw_function: DrawFunctionId,
pub batch_range: Range<u32>,
pub extra_index: PhaseItemExtraIndex,
pub index: usize,
pub indexed: bool,
}
impl PhaseItem for TransparentUi {
#[inline]
fn entity(&self) -> Entity {
self.entity.0
}
fn main_entity(&self) -> MainEntity {
self.entity.1
}
#[inline]
fn draw_function(&self) -> DrawFunctionId {
self.draw_function
}
#[inline]
fn batch_range(&self) -> &Range<u32> {
&self.batch_range
}
#[inline]
fn batch_range_mut(&mut self) -> &mut Range<u32> {
&mut self.batch_range
}
#[inline]
fn extra_index(&self) -> PhaseItemExtraIndex {
self.extra_index.clone()
}
#[inline]
fn batch_range_and_extra_index_mut(&mut self) -> (&mut Range<u32>, &mut PhaseItemExtraIndex) {
(&mut self.batch_range, &mut self.extra_index)
}
}
impl SortedPhaseItem for TransparentUi {
type SortKey = FloatOrd;
#[inline]
fn sort_key(&self) -> Self::SortKey {
self.sort_key
}
#[inline]
fn sort(items: &mut [Self]) {
items.sort_by_key(SortedPhaseItem::sort_key);
}
#[inline]
fn indexed(&self) -> bool {
self.indexed
}
}
impl CachedRenderPipelinePhaseItem for TransparentUi {
#[inline]
fn cached_pipeline(&self) -> CachedRenderPipelineId {
self.pipeline
}
}
pub type DrawUi = (
SetItemPipeline,
SetUiViewBindGroup<0>,
SetUiTextureBindGroup<1>,
DrawUiNode,
);
pub struct SetUiViewBindGroup<const I: usize>;
impl<P: PhaseItem, const I: usize> RenderCommand<P> for SetUiViewBindGroup<I> {
type Param = SRes<UiMeta>;
type ViewQuery = Read<ViewUniformOffset>;
type ItemQuery = ();
fn render<'w>(
_item: &P,
view_uniform: &'w ViewUniformOffset,
_entity: Option<()>,
ui_meta: SystemParamItem<'w, '_, Self::Param>,
pass: &mut TrackedRenderPass<'w>,
) -> RenderCommandResult {
let Some(view_bind_group) = ui_meta.into_inner().view_bind_group.as_ref() else {
return RenderCommandResult::Failure("view_bind_group not available");
};
pass.set_bind_group(I, view_bind_group, &[view_uniform.offset]);
RenderCommandResult::Success
}
}
pub struct SetUiTextureBindGroup<const I: usize>;
impl<P: PhaseItem, const I: usize> RenderCommand<P> for SetUiTextureBindGroup<I> {
type Param = SRes<ImageNodeBindGroups>;
type ViewQuery = ();
type ItemQuery = Read<UiBatch>;
#[inline]
fn render<'w>(
_item: &P,
_view: (),
batch: Option<&'w UiBatch>,
image_bind_groups: SystemParamItem<'w, '_, Self::Param>,
pass: &mut TrackedRenderPass<'w>,
) -> RenderCommandResult {
let image_bind_groups = image_bind_groups.into_inner();
let Some(batch) = batch else {
return RenderCommandResult::Skip;
};
pass.set_bind_group(I, image_bind_groups.values.get(&batch.image).unwrap(), &[]);
RenderCommandResult::Success
}
}
pub struct DrawUiNode;
impl<P: PhaseItem> RenderCommand<P> for DrawUiNode {
type Param = SRes<UiMeta>;
type ViewQuery = ();
type ItemQuery = Read<UiBatch>;
#[inline]
fn render<'w>(
_item: &P,
_view: (),
batch: Option<&'w UiBatch>,
ui_meta: SystemParamItem<'w, '_, Self::Param>,
pass: &mut TrackedRenderPass<'w>,
) -> RenderCommandResult {
let Some(batch) = batch else {
return RenderCommandResult::Skip;
};
let ui_meta = ui_meta.into_inner();
let Some(vertices) = ui_meta.vertices.buffer() else {
return RenderCommandResult::Failure("missing vertices to draw ui");
};
let Some(indices) = ui_meta.indices.buffer() else {
return RenderCommandResult::Failure("missing indices to draw ui");
};
// Store the vertices
pass.set_vertex_buffer(0, vertices.slice(..));
// Define how to "connect" the vertices
pass.set_index_buffer(
indices.slice(..),
0,
bevy_render::render_resource::IndexFormat::Uint32,
);
// Draw the vertices
pass.draw_indexed(batch.range.clone(), 0, 0..1);
RenderCommandResult::Success
}
}

184
vendor/bevy_ui/src/render/ui.wgsl vendored Normal file
View File

@@ -0,0 +1,184 @@
#import bevy_render::view::View
const TEXTURED = 1u;
const RIGHT_VERTEX = 2u;
const BOTTOM_VERTEX = 4u;
const BORDER: u32 = 8u;
fn enabled(flags: u32, mask: u32) -> bool {
return (flags & mask) != 0u;
}
@group(0) @binding(0) var<uniform> view: View;
struct VertexOutput {
@location(0) uv: vec2<f32>,
@location(1) color: vec4<f32>,
@location(2) @interpolate(flat) size: vec2<f32>,
@location(3) @interpolate(flat) flags: u32,
@location(4) @interpolate(flat) radius: vec4<f32>,
@location(5) @interpolate(flat) border: vec4<f32>,
// Position relative to the center of the rectangle.
@location(6) point: vec2<f32>,
@builtin(position) position: vec4<f32>,
};
@vertex
fn vertex(
@location(0) vertex_position: vec3<f32>,
@location(1) vertex_uv: vec2<f32>,
@location(2) vertex_color: vec4<f32>,
@location(3) flags: u32,
// x: top left, y: top right, z: bottom right, w: bottom left.
@location(4) radius: vec4<f32>,
// x: left, y: top, z: right, w: bottom.
@location(5) border: vec4<f32>,
@location(6) size: vec2<f32>,
@location(7) point: vec2<f32>,
) -> VertexOutput {
var out: VertexOutput;
out.uv = vertex_uv;
out.position = view.clip_from_world * vec4(vertex_position, 1.0);
out.color = vertex_color;
out.flags = flags;
out.radius = radius;
out.size = size;
out.border = border;
out.point = point;
return out;
}
@group(1) @binding(0) var sprite_texture: texture_2d<f32>;
@group(1) @binding(1) var sprite_sampler: sampler;
// The returned value is the shortest distance from the given point to the boundary of the rounded
// box.
//
// Negative values indicate that the point is inside the rounded box, positive values that the point
// is outside, and zero is exactly on the boundary.
//
// Arguments:
// - `point` -> The function will return the distance from this point to the closest point on
// the boundary.
// - `size` -> The maximum width and height of the box.
// - `corner_radii` -> The radius of each rounded corner. Ordered counter clockwise starting
// top left:
// x: top left, y: top right, z: bottom right, w: bottom left.
fn sd_rounded_box(point: vec2<f32>, size: vec2<f32>, corner_radii: vec4<f32>) -> f32 {
// If 0.0 < y then select bottom left (w) and bottom right corner radius (z).
// Else select top left (x) and top right corner radius (y).
let rs = select(corner_radii.xy, corner_radii.wz, 0.0 < point.y);
// w and z are swapped above so that both pairs are in left to right order, otherwise this second
// select statement would return the incorrect value for the bottom pair.
let radius = select(rs.x, rs.y, 0.0 < point.x);
// Vector from the corner closest to the point, to the point.
let corner_to_point = abs(point) - 0.5 * size;
// Vector from the center of the radius circle to the point.
let q = corner_to_point + radius;
// Length from center of the radius circle to the point, zeros a component if the point is not
// within the quadrant of the radius circle that is part of the curved corner.
let l = length(max(q, vec2(0.0)));
let m = min(max(q.x, q.y), 0.0);
return l + m - radius;
}
fn sd_inset_rounded_box(point: vec2<f32>, size: vec2<f32>, radius: vec4<f32>, inset: vec4<f32>) -> f32 {
let inner_size = size - inset.xy - inset.zw;
let inner_center = inset.xy + 0.5 * inner_size - 0.5 * size;
let inner_point = point - inner_center;
var r = radius;
// Top left corner.
r.x = r.x - max(inset.x, inset.y);
// Top right corner.
r.y = r.y - max(inset.z, inset.y);
// Bottom right corner.
r.z = r.z - max(inset.z, inset.w);
// Bottom left corner.
r.w = r.w - max(inset.x, inset.w);
let half_size = inner_size * 0.5;
let min_size = min(half_size.x, half_size.y);
r = min(max(r, vec4(0.0)), vec4<f32>(min_size));
return sd_rounded_box(inner_point, inner_size, r);
}
// get alpha for antialiasing for sdf
fn antialias(distance: f32) -> f32 {
// Using the fwidth(distance) was causing artifacts, so just use the distance.
return saturate(0.5 - distance);
}
fn draw(in: VertexOutput, texture_color: vec4<f32>) -> vec4<f32> {
// Only use the color sampled from the texture if the `TEXTURED` flag is enabled.
// This allows us to draw both textured and untextured shapes together in the same batch.
let color = select(in.color, in.color * texture_color, enabled(in.flags, TEXTURED));
// Signed distances. The magnitude is the distance of the point from the edge of the shape.
// * Negative values indicate that the point is inside the shape.
// * Zero values indicate the point is on the edge of the shape.
// * Positive values indicate the point is outside the shape.
// Signed distance from the exterior boundary.
let external_distance = sd_rounded_box(in.point, in.size, in.radius);
// Signed distance from the border's internal edge (the signed distance is negative if the point
// is inside the rect but not on the border).
// If the border size is set to zero, this is the same as the external distance.
let internal_distance = sd_inset_rounded_box(in.point, in.size, in.radius, in.border);
// Signed distance from the border (the intersection of the rect with its border).
// Points inside the border have negative signed distance. Any point outside the border, whether
// outside the outside edge, or inside the inner edge have positive signed distance.
let border_distance = max(external_distance, -internal_distance);
#ifdef ANTI_ALIAS
// At external edges with no border, `border_distance` is equal to zero.
// This select statement ensures we only perform anti-aliasing where a non-zero width border
// is present, otherwise an outline about the external boundary would be drawn even without
// a border.
let t = select(1.0 - step(0.0, border_distance), antialias(border_distance), external_distance < internal_distance);
#else
let t = 1.0 - step(0.0, border_distance);
#endif
// Blend mode ALPHA_BLENDING is used for UI elements, so we don't premultiply alpha here.
return vec4(color.rgb, saturate(color.a * t));
}
fn draw_background(in: VertexOutput, texture_color: vec4<f32>) -> vec4<f32> {
let color = select(in.color, in.color * texture_color, enabled(in.flags, TEXTURED));
// When drawing the background only draw the internal area and not the border.
let internal_distance = sd_inset_rounded_box(in.point, in.size, in.radius, in.border);
#ifdef ANTI_ALIAS
let t = antialias(internal_distance);
#else
let t = 1.0 - step(0.0, internal_distance);
#endif
return vec4(color.rgb, saturate(color.a * t));
}
@fragment
fn fragment(in: VertexOutput) -> @location(0) vec4<f32> {
let texture_color = textureSample(sprite_texture, sprite_sampler, in.uv);
if enabled(in.flags, BORDER) {
return draw(in, texture_color);
} else {
return draw_background(in, texture_color);
}
}

View File

@@ -0,0 +1,32 @@
#import bevy_render::{
view::View,
globals::Globals,
}
#import bevy_ui::ui_vertex_output::UiVertexOutput
@group(0) @binding(0)
var<uniform> view: View;
@group(0) @binding(1)
var<uniform> globals: Globals;
@vertex
fn vertex(
@location(0) vertex_position: vec3<f32>,
@location(1) vertex_uv: vec2<f32>,
@location(2) size: vec2<f32>,
@location(3) border_widths: vec4<f32>,
@location(4) border_radius: vec4<f32>,
) -> UiVertexOutput {
var out: UiVertexOutput;
out.uv = vertex_uv;
out.position = view.clip_from_world * vec4<f32>(vertex_position, 1.0);
out.size = size;
out.border_widths = border_widths;
out.border_radius = border_radius;
return out;
}
@fragment
fn fragment(in: UiVertexOutput) -> @location(0) vec4<f32> {
return vec4<f32>(1.0);
}

View File

@@ -0,0 +1,675 @@
use core::{hash::Hash, marker::PhantomData, ops::Range};
use crate::*;
use bevy_asset::*;
use bevy_ecs::{
prelude::Component,
query::ROQueryItem,
system::{
lifetimeless::{Read, SRes},
*,
},
};
use bevy_image::BevyDefault as _;
use bevy_math::{FloatOrd, Mat4, Rect, Vec2, Vec4Swizzles};
use bevy_render::sync_world::{MainEntity, TemporaryRenderEntity};
use bevy_render::{
extract_component::ExtractComponentPlugin,
globals::{GlobalsBuffer, GlobalsUniform},
render_asset::{PrepareAssetError, RenderAsset, RenderAssetPlugin, RenderAssets},
render_phase::*,
render_resource::{binding_types::uniform_buffer, *},
renderer::{RenderDevice, RenderQueue},
view::*,
Extract, ExtractSchedule, Render, RenderSet,
};
use bevy_sprite::BorderRect;
use bevy_transform::prelude::GlobalTransform;
use bytemuck::{Pod, Zeroable};
pub const UI_MATERIAL_SHADER_HANDLE: Handle<Shader> =
weak_handle!("b5612b7b-aed5-41b4-a930-1d1588239fcd");
const UI_VERTEX_OUTPUT_SHADER_HANDLE: Handle<Shader> =
weak_handle!("1d97ca3e-eaa8-4bc5-a676-e8e9568c472e");
/// Adds the necessary ECS resources and render logic to enable rendering entities using the given
/// [`UiMaterial`] asset type (which includes [`UiMaterial`] types).
pub struct UiMaterialPlugin<M: UiMaterial>(PhantomData<M>);
impl<M: UiMaterial> Default for UiMaterialPlugin<M> {
fn default() -> Self {
Self(Default::default())
}
}
impl<M: UiMaterial> Plugin for UiMaterialPlugin<M>
where
M::Data: PartialEq + Eq + Hash + Clone,
{
fn build(&self, app: &mut App) {
load_internal_asset!(
app,
UI_VERTEX_OUTPUT_SHADER_HANDLE,
"ui_vertex_output.wgsl",
Shader::from_wgsl
);
load_internal_asset!(
app,
UI_MATERIAL_SHADER_HANDLE,
"ui_material.wgsl",
Shader::from_wgsl
);
app.init_asset::<M>()
.register_type::<MaterialNode<M>>()
.add_plugins((
ExtractComponentPlugin::<MaterialNode<M>>::extract_visible(),
RenderAssetPlugin::<PreparedUiMaterial<M>>::default(),
));
if let Some(render_app) = app.get_sub_app_mut(RenderApp) {
render_app
.add_render_command::<TransparentUi, DrawUiMaterial<M>>()
.init_resource::<ExtractedUiMaterialNodes<M>>()
.init_resource::<UiMaterialMeta<M>>()
.init_resource::<SpecializedRenderPipelines<UiMaterialPipeline<M>>>()
.add_systems(
ExtractSchedule,
extract_ui_material_nodes::<M>.in_set(RenderUiSystem::ExtractBackgrounds),
)
.add_systems(
Render,
(
queue_ui_material_nodes::<M>.in_set(RenderSet::Queue),
prepare_uimaterial_nodes::<M>.in_set(RenderSet::PrepareBindGroups),
),
);
}
}
fn finish(&self, app: &mut App) {
if let Some(render_app) = app.get_sub_app_mut(RenderApp) {
render_app.init_resource::<UiMaterialPipeline<M>>();
}
}
}
#[derive(Resource)]
pub struct UiMaterialMeta<M: UiMaterial> {
vertices: RawBufferVec<UiMaterialVertex>,
view_bind_group: Option<BindGroup>,
marker: PhantomData<M>,
}
impl<M: UiMaterial> Default for UiMaterialMeta<M> {
fn default() -> Self {
Self {
vertices: RawBufferVec::new(BufferUsages::VERTEX),
view_bind_group: Default::default(),
marker: PhantomData,
}
}
}
#[repr(C)]
#[derive(Copy, Clone, Pod, Zeroable)]
pub struct UiMaterialVertex {
pub position: [f32; 3],
pub uv: [f32; 2],
pub size: [f32; 2],
pub border: [f32; 4],
pub radius: [f32; 4],
}
// in this [`UiMaterialPipeline`] there is (currently) no batching going on.
// Therefore the [`UiMaterialBatch`] is more akin to a draw call.
#[derive(Component)]
pub struct UiMaterialBatch<M: UiMaterial> {
/// The range of vertices inside the [`UiMaterialMeta`]
pub range: Range<u32>,
pub material: AssetId<M>,
}
/// Render pipeline data for a given [`UiMaterial`]
#[derive(Resource)]
pub struct UiMaterialPipeline<M: UiMaterial> {
pub ui_layout: BindGroupLayout,
pub view_layout: BindGroupLayout,
pub vertex_shader: Option<Handle<Shader>>,
pub fragment_shader: Option<Handle<Shader>>,
marker: PhantomData<M>,
}
impl<M: UiMaterial> SpecializedRenderPipeline for UiMaterialPipeline<M>
where
M::Data: PartialEq + Eq + Hash + Clone,
{
type Key = UiMaterialKey<M>;
fn specialize(&self, key: Self::Key) -> RenderPipelineDescriptor {
let vertex_layout = VertexBufferLayout::from_vertex_formats(
VertexStepMode::Vertex,
vec![
// position
VertexFormat::Float32x3,
// uv
VertexFormat::Float32x2,
// size
VertexFormat::Float32x2,
// border widths
VertexFormat::Float32x4,
// border radius
VertexFormat::Float32x4,
],
);
let shader_defs = Vec::new();
let mut descriptor = RenderPipelineDescriptor {
vertex: VertexState {
shader: UI_MATERIAL_SHADER_HANDLE,
entry_point: "vertex".into(),
shader_defs: shader_defs.clone(),
buffers: vec![vertex_layout],
},
fragment: Some(FragmentState {
shader: UI_MATERIAL_SHADER_HANDLE,
shader_defs,
entry_point: "fragment".into(),
targets: vec![Some(ColorTargetState {
format: if key.hdr {
ViewTarget::TEXTURE_FORMAT_HDR
} else {
TextureFormat::bevy_default()
},
blend: Some(BlendState::ALPHA_BLENDING),
write_mask: ColorWrites::ALL,
})],
}),
layout: vec![],
push_constant_ranges: Vec::new(),
primitive: PrimitiveState {
front_face: FrontFace::Ccw,
cull_mode: None,
unclipped_depth: false,
polygon_mode: PolygonMode::Fill,
conservative: false,
topology: PrimitiveTopology::TriangleList,
strip_index_format: None,
},
depth_stencil: None,
multisample: MultisampleState {
count: 1,
mask: !0,
alpha_to_coverage_enabled: false,
},
label: Some("ui_material_pipeline".into()),
zero_initialize_workgroup_memory: false,
};
if let Some(vertex_shader) = &self.vertex_shader {
descriptor.vertex.shader = vertex_shader.clone();
}
if let Some(fragment_shader) = &self.fragment_shader {
descriptor.fragment.as_mut().unwrap().shader = fragment_shader.clone();
}
descriptor.layout = vec![self.view_layout.clone(), self.ui_layout.clone()];
M::specialize(&mut descriptor, key);
descriptor
}
}
impl<M: UiMaterial> FromWorld for UiMaterialPipeline<M> {
fn from_world(world: &mut World) -> Self {
let asset_server = world.resource::<AssetServer>();
let render_device = world.resource::<RenderDevice>();
let ui_layout = M::bind_group_layout(render_device);
let view_layout = render_device.create_bind_group_layout(
"ui_view_layout",
&BindGroupLayoutEntries::sequential(
ShaderStages::VERTEX_FRAGMENT,
(
uniform_buffer::<ViewUniform>(true),
uniform_buffer::<GlobalsUniform>(false),
),
),
);
UiMaterialPipeline {
ui_layout,
view_layout,
vertex_shader: match M::vertex_shader() {
ShaderRef::Default => None,
ShaderRef::Handle(handle) => Some(handle),
ShaderRef::Path(path) => Some(asset_server.load(path)),
},
fragment_shader: match M::fragment_shader() {
ShaderRef::Default => None,
ShaderRef::Handle(handle) => Some(handle),
ShaderRef::Path(path) => Some(asset_server.load(path)),
},
marker: PhantomData,
}
}
}
pub type DrawUiMaterial<M> = (
SetItemPipeline,
SetMatUiViewBindGroup<M, 0>,
SetUiMaterialBindGroup<M, 1>,
DrawUiMaterialNode<M>,
);
pub struct SetMatUiViewBindGroup<M: UiMaterial, const I: usize>(PhantomData<M>);
impl<P: PhaseItem, M: UiMaterial, const I: usize> RenderCommand<P> for SetMatUiViewBindGroup<M, I> {
type Param = SRes<UiMaterialMeta<M>>;
type ViewQuery = Read<ViewUniformOffset>;
type ItemQuery = ();
fn render<'w>(
_item: &P,
view_uniform: &'w ViewUniformOffset,
_entity: Option<()>,
ui_meta: SystemParamItem<'w, '_, Self::Param>,
pass: &mut TrackedRenderPass<'w>,
) -> RenderCommandResult {
pass.set_bind_group(
I,
ui_meta.into_inner().view_bind_group.as_ref().unwrap(),
&[view_uniform.offset],
);
RenderCommandResult::Success
}
}
pub struct SetUiMaterialBindGroup<M: UiMaterial, const I: usize>(PhantomData<M>);
impl<P: PhaseItem, M: UiMaterial, const I: usize> RenderCommand<P>
for SetUiMaterialBindGroup<M, I>
{
type Param = SRes<RenderAssets<PreparedUiMaterial<M>>>;
type ViewQuery = ();
type ItemQuery = Read<UiMaterialBatch<M>>;
fn render<'w>(
_item: &P,
_view: (),
material_handle: Option<ROQueryItem<'_, Self::ItemQuery>>,
materials: SystemParamItem<'w, '_, Self::Param>,
pass: &mut TrackedRenderPass<'w>,
) -> RenderCommandResult {
let Some(material_handle) = material_handle else {
return RenderCommandResult::Skip;
};
let Some(material) = materials.into_inner().get(material_handle.material) else {
return RenderCommandResult::Skip;
};
pass.set_bind_group(I, &material.bind_group, &[]);
RenderCommandResult::Success
}
}
pub struct DrawUiMaterialNode<M>(PhantomData<M>);
impl<P: PhaseItem, M: UiMaterial> RenderCommand<P> for DrawUiMaterialNode<M> {
type Param = SRes<UiMaterialMeta<M>>;
type ViewQuery = ();
type ItemQuery = Read<UiMaterialBatch<M>>;
#[inline]
fn render<'w>(
_item: &P,
_view: (),
batch: Option<&'w UiMaterialBatch<M>>,
ui_meta: SystemParamItem<'w, '_, Self::Param>,
pass: &mut TrackedRenderPass<'w>,
) -> RenderCommandResult {
let Some(batch) = batch else {
return RenderCommandResult::Skip;
};
pass.set_vertex_buffer(0, ui_meta.into_inner().vertices.buffer().unwrap().slice(..));
pass.draw(batch.range.clone(), 0..1);
RenderCommandResult::Success
}
}
pub struct ExtractedUiMaterialNode<M: UiMaterial> {
pub stack_index: u32,
pub transform: Mat4,
pub rect: Rect,
pub border: BorderRect,
pub border_radius: ResolvedBorderRadius,
pub material: AssetId<M>,
pub clip: Option<Rect>,
// Camera to render this UI node to. By the time it is extracted,
// it is defaulted to a single camera if only one exists.
// Nodes with ambiguous camera will be ignored.
pub extracted_camera_entity: Entity,
pub main_entity: MainEntity,
render_entity: Entity,
}
#[derive(Resource)]
pub struct ExtractedUiMaterialNodes<M: UiMaterial> {
pub uinodes: Vec<ExtractedUiMaterialNode<M>>,
}
impl<M: UiMaterial> Default for ExtractedUiMaterialNodes<M> {
fn default() -> Self {
Self {
uinodes: Default::default(),
}
}
}
pub fn extract_ui_material_nodes<M: UiMaterial>(
mut commands: Commands,
mut extracted_uinodes: ResMut<ExtractedUiMaterialNodes<M>>,
materials: Extract<Res<Assets<M>>>,
uinode_query: Extract<
Query<(
Entity,
&ComputedNode,
&GlobalTransform,
&MaterialNode<M>,
&InheritedVisibility,
Option<&CalculatedClip>,
&ComputedNodeTarget,
)>,
>,
camera_map: Extract<UiCameraMap>,
) {
let mut camera_mapper = camera_map.get_mapper();
for (entity, computed_node, transform, handle, inherited_visibility, clip, camera) in
uinode_query.iter()
{
// skip invisible nodes
if !inherited_visibility.get() || computed_node.is_empty() {
continue;
}
// Skip loading materials
if !materials.contains(handle) {
continue;
}
let Some(extracted_camera_entity) = camera_mapper.map(camera) else {
continue;
};
extracted_uinodes.uinodes.push(ExtractedUiMaterialNode {
render_entity: commands.spawn(TemporaryRenderEntity).id(),
stack_index: computed_node.stack_index,
transform: transform.compute_matrix(),
material: handle.id(),
rect: Rect {
min: Vec2::ZERO,
max: computed_node.size(),
},
border: computed_node.border(),
border_radius: computed_node.border_radius(),
clip: clip.map(|clip| clip.clip),
extracted_camera_entity,
main_entity: entity.into(),
});
}
}
pub fn prepare_uimaterial_nodes<M: UiMaterial>(
mut commands: Commands,
render_device: Res<RenderDevice>,
render_queue: Res<RenderQueue>,
mut ui_meta: ResMut<UiMaterialMeta<M>>,
mut extracted_uinodes: ResMut<ExtractedUiMaterialNodes<M>>,
view_uniforms: Res<ViewUniforms>,
globals_buffer: Res<GlobalsBuffer>,
ui_material_pipeline: Res<UiMaterialPipeline<M>>,
mut phases: ResMut<ViewSortedRenderPhases<TransparentUi>>,
mut previous_len: Local<usize>,
) {
if let (Some(view_binding), Some(globals_binding)) = (
view_uniforms.uniforms.binding(),
globals_buffer.buffer.binding(),
) {
let mut batches: Vec<(Entity, UiMaterialBatch<M>)> = Vec::with_capacity(*previous_len);
ui_meta.vertices.clear();
ui_meta.view_bind_group = Some(render_device.create_bind_group(
"ui_material_view_bind_group",
&ui_material_pipeline.view_layout,
&BindGroupEntries::sequential((view_binding, globals_binding)),
));
let mut index = 0;
for ui_phase in phases.values_mut() {
let mut batch_item_index = 0;
let mut batch_shader_handle = AssetId::invalid();
for item_index in 0..ui_phase.items.len() {
let item = &mut ui_phase.items[item_index];
if let Some(extracted_uinode) = extracted_uinodes
.uinodes
.get(item.index)
.filter(|n| item.entity() == n.render_entity)
{
let mut existing_batch = batches
.last_mut()
.filter(|_| batch_shader_handle == extracted_uinode.material);
if existing_batch.is_none() {
batch_item_index = item_index;
batch_shader_handle = extracted_uinode.material;
let new_batch = UiMaterialBatch {
range: index..index,
material: extracted_uinode.material,
};
batches.push((item.entity(), new_batch));
existing_batch = batches.last_mut();
}
let uinode_rect = extracted_uinode.rect;
let rect_size = uinode_rect.size().extend(1.0);
let positions = QUAD_VERTEX_POSITIONS.map(|pos| {
(extracted_uinode.transform * (pos * rect_size).extend(1.0)).xyz()
});
let positions_diff = if let Some(clip) = extracted_uinode.clip {
[
Vec2::new(
f32::max(clip.min.x - positions[0].x, 0.),
f32::max(clip.min.y - positions[0].y, 0.),
),
Vec2::new(
f32::min(clip.max.x - positions[1].x, 0.),
f32::max(clip.min.y - positions[1].y, 0.),
),
Vec2::new(
f32::min(clip.max.x - positions[2].x, 0.),
f32::min(clip.max.y - positions[2].y, 0.),
),
Vec2::new(
f32::max(clip.min.x - positions[3].x, 0.),
f32::min(clip.max.y - positions[3].y, 0.),
),
]
} else {
[Vec2::ZERO; 4]
};
let positions_clipped = [
positions[0] + positions_diff[0].extend(0.),
positions[1] + positions_diff[1].extend(0.),
positions[2] + positions_diff[2].extend(0.),
positions[3] + positions_diff[3].extend(0.),
];
let transformed_rect_size =
extracted_uinode.transform.transform_vector3(rect_size);
// Don't try to cull nodes that have a rotation
// In a rotation around the Z-axis, this value is 0.0 for an angle of 0.0 or π
// In those two cases, the culling check can proceed normally as corners will be on
// horizontal / vertical lines
// For all other angles, bypass the culling check
// This does not properly handles all rotations on all axis
if extracted_uinode.transform.x_axis[1] == 0.0 {
// Cull nodes that are completely clipped
if positions_diff[0].x - positions_diff[1].x >= transformed_rect_size.x
|| positions_diff[1].y - positions_diff[2].y >= transformed_rect_size.y
{
continue;
}
}
let uvs = [
Vec2::new(
uinode_rect.min.x + positions_diff[0].x,
uinode_rect.min.y + positions_diff[0].y,
),
Vec2::new(
uinode_rect.max.x + positions_diff[1].x,
uinode_rect.min.y + positions_diff[1].y,
),
Vec2::new(
uinode_rect.max.x + positions_diff[2].x,
uinode_rect.max.y + positions_diff[2].y,
),
Vec2::new(
uinode_rect.min.x + positions_diff[3].x,
uinode_rect.max.y + positions_diff[3].y,
),
]
.map(|pos| pos / uinode_rect.max);
for i in QUAD_INDICES {
ui_meta.vertices.push(UiMaterialVertex {
position: positions_clipped[i].into(),
uv: uvs[i].into(),
size: extracted_uinode.rect.size().into(),
radius: [
extracted_uinode.border_radius.top_left,
extracted_uinode.border_radius.top_right,
extracted_uinode.border_radius.bottom_right,
extracted_uinode.border_radius.bottom_left,
],
border: [
extracted_uinode.border.left,
extracted_uinode.border.top,
extracted_uinode.border.right,
extracted_uinode.border.bottom,
],
});
}
index += QUAD_INDICES.len() as u32;
existing_batch.unwrap().1.range.end = index;
ui_phase.items[batch_item_index].batch_range_mut().end += 1;
} else {
batch_shader_handle = AssetId::invalid();
}
}
}
ui_meta.vertices.write_buffer(&render_device, &render_queue);
*previous_len = batches.len();
commands.try_insert_batch(batches);
}
extracted_uinodes.uinodes.clear();
}
pub struct PreparedUiMaterial<T: UiMaterial> {
pub bindings: BindingResources,
pub bind_group: BindGroup,
pub key: T::Data,
}
impl<M: UiMaterial> RenderAsset for PreparedUiMaterial<M> {
type SourceAsset = M;
type Param = (SRes<RenderDevice>, SRes<UiMaterialPipeline<M>>, M::Param);
fn prepare_asset(
material: Self::SourceAsset,
_: AssetId<Self::SourceAsset>,
(render_device, pipeline, material_param): &mut SystemParamItem<Self::Param>,
) -> Result<Self, PrepareAssetError<Self::SourceAsset>> {
match material.as_bind_group(&pipeline.ui_layout, render_device, material_param) {
Ok(prepared) => Ok(PreparedUiMaterial {
bindings: prepared.bindings,
bind_group: prepared.bind_group,
key: prepared.data,
}),
Err(AsBindGroupError::RetryNextUpdate) => {
Err(PrepareAssetError::RetryNextUpdate(material))
}
Err(other) => Err(PrepareAssetError::AsBindGroupError(other)),
}
}
}
pub fn queue_ui_material_nodes<M: UiMaterial>(
extracted_uinodes: Res<ExtractedUiMaterialNodes<M>>,
draw_functions: Res<DrawFunctions<TransparentUi>>,
ui_material_pipeline: Res<UiMaterialPipeline<M>>,
mut pipelines: ResMut<SpecializedRenderPipelines<UiMaterialPipeline<M>>>,
pipeline_cache: Res<PipelineCache>,
render_materials: Res<RenderAssets<PreparedUiMaterial<M>>>,
mut transparent_render_phases: ResMut<ViewSortedRenderPhases<TransparentUi>>,
mut render_views: Query<&UiCameraView, With<ExtractedView>>,
camera_views: Query<&ExtractedView>,
) where
M::Data: PartialEq + Eq + Hash + Clone,
{
let draw_function = draw_functions.read().id::<DrawUiMaterial<M>>();
for (index, extracted_uinode) in extracted_uinodes.uinodes.iter().enumerate() {
let Some(material) = render_materials.get(extracted_uinode.material) else {
continue;
};
let Ok(default_camera_view) =
render_views.get_mut(extracted_uinode.extracted_camera_entity)
else {
continue;
};
let Ok(view) = camera_views.get(default_camera_view.0) else {
continue;
};
let Some(transparent_phase) = transparent_render_phases.get_mut(&view.retained_view_entity)
else {
continue;
};
let pipeline = pipelines.specialize(
&pipeline_cache,
&ui_material_pipeline,
UiMaterialKey {
hdr: view.hdr,
bind_group_data: material.key.clone(),
},
);
if transparent_phase.items.capacity() < extracted_uinodes.uinodes.len() {
transparent_phase.items.reserve_exact(
extracted_uinodes.uinodes.len() - transparent_phase.items.capacity(),
);
}
transparent_phase.add(TransparentUi {
draw_function,
pipeline,
entity: (extracted_uinode.render_entity, extracted_uinode.main_entity),
sort_key: FloatOrd(extracted_uinode.stack_index as f32 + stack_z_offsets::MATERIAL),
batch_range: 0..0,
extra_index: PhaseItemExtraIndex::None,
index,
indexed: false,
});
}
}

View File

@@ -0,0 +1,127 @@
#import bevy_render::view::View;
#import bevy_render::globals::Globals;
@group(0) @binding(0)
var<uniform> view: View;
@group(0) @binding(1)
var<uniform> globals: Globals;
@group(1) @binding(0) var sprite_texture: texture_2d<f32>;
@group(1) @binding(1) var sprite_sampler: sampler;
struct UiVertexOutput {
@location(0) uv: vec2<f32>,
@location(1) color: vec4<f32>,
// Defines the dividing line that are used to split the texture atlas rect into corner, side and center slices
// The distances are normalized and from the top left corner of the texture atlas rect
// x = distance of the left vertical dividing line
// y = distance of the top horizontal dividing line
// z = distance of the right vertical dividing line
// w = distance of the bottom horizontal dividing line
@location(2) @interpolate(flat) texture_slices: vec4<f32>,
// Defines the dividing line that are used to split the render target into corner, side and center slices
// The distances are normalized and from the top left corner of the render target
// x = distance of left vertical dividing line
// y = distance of top horizontal dividing line
// z = distance of right vertical dividing line
// w = distance of bottom horizontal dividing line
@location(3) @interpolate(flat) target_slices: vec4<f32>,
// The number of times the side or center texture slices should be repeated when mapping them to the border slices
// x = number of times to repeat along the horizontal axis for the side textures
// y = number of times to repeat along the vertical axis for the side textures
// z = number of times to repeat along the horizontal axis for the center texture
// w = number of times to repeat along the vertical axis for the center texture
@location(4) @interpolate(flat) repeat: vec4<f32>,
// normalized texture atlas rect coordinates
// x, y = top, left corner of the atlas rect
// z, w = bottom, right corner of the atlas rect
@location(5) @interpolate(flat) atlas_rect: vec4<f32>,
@builtin(position) position: vec4<f32>,
}
@vertex
fn vertex(
@location(0) vertex_position: vec3<f32>,
@location(1) vertex_uv: vec2<f32>,
@location(2) vertex_color: vec4<f32>,
@location(3) texture_slices: vec4<f32>,
@location(4) target_slices: vec4<f32>,
@location(5) repeat: vec4<f32>,
@location(6) atlas_rect: vec4<f32>,
) -> UiVertexOutput {
var out: UiVertexOutput;
out.uv = vertex_uv;
out.color = vertex_color;
out.position = view.clip_from_world * vec4<f32>(vertex_position, 1.0);
out.texture_slices = texture_slices;
out.target_slices = target_slices;
out.repeat = repeat;
out.atlas_rect = atlas_rect;
return out;
}
/// maps a point along the axis of the render target to slice coordinates
fn map_axis_with_repeat(
// normalized distance along the axis
p: f32,
// target min dividing point
il: f32,
// target max dividing point
ih: f32,
// slice min dividing point
tl: f32,
// slice max dividing point
th: f32,
// number of times to repeat the slice for sides and the center
r: f32,
) -> f32 {
if p < il {
// inside one of the two left (horizontal axis) or top (vertical axis) corners
return (p / il) * tl;
} else if ih < p {
// inside one of the two (horizontal axis) or top (vertical axis) corners
return th + ((p - ih) / (1 - ih)) * (1 - th);
} else {
// not inside a corner, repeat the texture
return tl + fract((r * (p - il)) / (ih - il)) * (th - tl);
}
}
fn map_uvs_to_slice(
uv: vec2<f32>,
target_slices: vec4<f32>,
texture_slices: vec4<f32>,
repeat: vec4<f32>,
) -> vec2<f32> {
var r: vec2<f32>;
if target_slices.x <= uv.x && uv.x <= target_slices.z && target_slices.y <= uv.y && uv.y <= target_slices.w {
// use the center repeat values if the uv coords are inside the center slice of the target
r = repeat.zw;
} else {
// use the side repeat values if the uv coords are outside the center slice
r = repeat.xy;
}
// map horizontal axis
let x = map_axis_with_repeat(uv.x, target_slices.x, target_slices.z, texture_slices.x, texture_slices.z, r.x);
// map vertical axis
let y = map_axis_with_repeat(uv.y, target_slices.y, target_slices.w, texture_slices.y, texture_slices.w, r.y);
return vec2(x, y);
}
@fragment
fn fragment(in: UiVertexOutput) -> @location(0) vec4<f32> {
// map the target uvs to slice coords
let uv = map_uvs_to_slice(in.uv, in.target_slices, in.texture_slices, in.repeat);
// map the slice coords to texture coords
let atlas_uv = in.atlas_rect.xy + uv * (in.atlas_rect.zw - in.atlas_rect.xy);
return in.color * textureSample(sprite_texture, sprite_sampler, atlas_uv);
}

View File

@@ -0,0 +1,835 @@
use core::{hash::Hash, ops::Range};
use crate::*;
use bevy_asset::*;
use bevy_color::{Alpha, ColorToComponents, LinearRgba};
use bevy_ecs::{
prelude::Component,
system::{
lifetimeless::{Read, SRes},
*,
},
};
use bevy_image::prelude::*;
use bevy_math::{FloatOrd, Mat4, Rect, Vec2, Vec4Swizzles};
use bevy_platform::collections::HashMap;
use bevy_render::sync_world::MainEntity;
use bevy_render::{
render_asset::RenderAssets,
render_phase::*,
render_resource::{binding_types::uniform_buffer, *},
renderer::{RenderDevice, RenderQueue},
sync_world::TemporaryRenderEntity,
texture::{GpuImage, TRANSPARENT_IMAGE_HANDLE},
view::*,
Extract, ExtractSchedule, Render, RenderSet,
};
use bevy_sprite::{SliceScaleMode, SpriteAssetEvents, SpriteImageMode, TextureSlicer};
use bevy_transform::prelude::GlobalTransform;
use binding_types::{sampler, texture_2d};
use bytemuck::{Pod, Zeroable};
use widget::ImageNode;
pub const UI_SLICER_SHADER_HANDLE: Handle<Shader> =
weak_handle!("10cd61e3-bbf7-47fa-91c8-16cbe806378c");
pub struct UiTextureSlicerPlugin;
impl Plugin for UiTextureSlicerPlugin {
fn build(&self, app: &mut App) {
load_internal_asset!(
app,
UI_SLICER_SHADER_HANDLE,
"ui_texture_slice.wgsl",
Shader::from_wgsl
);
if let Some(render_app) = app.get_sub_app_mut(RenderApp) {
render_app
.add_render_command::<TransparentUi, DrawUiTextureSlices>()
.init_resource::<ExtractedUiTextureSlices>()
.init_resource::<UiTextureSliceMeta>()
.init_resource::<UiTextureSliceImageBindGroups>()
.init_resource::<SpecializedRenderPipelines<UiTextureSlicePipeline>>()
.add_systems(
ExtractSchedule,
extract_ui_texture_slices.in_set(RenderUiSystem::ExtractTextureSlice),
)
.add_systems(
Render,
(
queue_ui_slices.in_set(RenderSet::Queue),
prepare_ui_slices.in_set(RenderSet::PrepareBindGroups),
),
);
}
}
fn finish(&self, app: &mut App) {
if let Some(render_app) = app.get_sub_app_mut(RenderApp) {
render_app.init_resource::<UiTextureSlicePipeline>();
}
}
}
#[repr(C)]
#[derive(Copy, Clone, Pod, Zeroable)]
struct UiTextureSliceVertex {
pub position: [f32; 3],
pub uv: [f32; 2],
pub color: [f32; 4],
pub slices: [f32; 4],
pub border: [f32; 4],
pub repeat: [f32; 4],
pub atlas: [f32; 4],
}
#[derive(Component)]
pub struct UiTextureSlicerBatch {
pub range: Range<u32>,
pub image: AssetId<Image>,
}
#[derive(Resource)]
pub struct UiTextureSliceMeta {
vertices: RawBufferVec<UiTextureSliceVertex>,
indices: RawBufferVec<u32>,
view_bind_group: Option<BindGroup>,
}
impl Default for UiTextureSliceMeta {
fn default() -> Self {
Self {
vertices: RawBufferVec::new(BufferUsages::VERTEX),
indices: RawBufferVec::new(BufferUsages::INDEX),
view_bind_group: None,
}
}
}
#[derive(Resource, Default)]
pub struct UiTextureSliceImageBindGroups {
pub values: HashMap<AssetId<Image>, BindGroup>,
}
#[derive(Resource)]
pub struct UiTextureSlicePipeline {
pub view_layout: BindGroupLayout,
pub image_layout: BindGroupLayout,
}
impl FromWorld for UiTextureSlicePipeline {
fn from_world(world: &mut World) -> Self {
let render_device = world.resource::<RenderDevice>();
let view_layout = render_device.create_bind_group_layout(
"ui_texture_slice_view_layout",
&BindGroupLayoutEntries::single(
ShaderStages::VERTEX_FRAGMENT,
uniform_buffer::<ViewUniform>(true),
),
);
let image_layout = render_device.create_bind_group_layout(
"ui_texture_slice_image_layout",
&BindGroupLayoutEntries::sequential(
ShaderStages::FRAGMENT,
(
texture_2d(TextureSampleType::Float { filterable: true }),
sampler(SamplerBindingType::Filtering),
),
),
);
UiTextureSlicePipeline {
view_layout,
image_layout,
}
}
}
#[derive(Clone, Copy, Hash, PartialEq, Eq)]
pub struct UiTextureSlicePipelineKey {
pub hdr: bool,
}
impl SpecializedRenderPipeline for UiTextureSlicePipeline {
type Key = UiTextureSlicePipelineKey;
fn specialize(&self, key: Self::Key) -> RenderPipelineDescriptor {
let vertex_layout = VertexBufferLayout::from_vertex_formats(
VertexStepMode::Vertex,
vec![
// position
VertexFormat::Float32x3,
// uv
VertexFormat::Float32x2,
// color
VertexFormat::Float32x4,
// normalized texture slicing lines (left, top, right, bottom)
VertexFormat::Float32x4,
// normalized target slicing lines (left, top, right, bottom)
VertexFormat::Float32x4,
// repeat values (horizontal side, vertical side, horizontal center, vertical center)
VertexFormat::Float32x4,
// normalized texture atlas rect (left, top, right, bottom)
VertexFormat::Float32x4,
],
);
let shader_defs = Vec::new();
RenderPipelineDescriptor {
vertex: VertexState {
shader: UI_SLICER_SHADER_HANDLE,
entry_point: "vertex".into(),
shader_defs: shader_defs.clone(),
buffers: vec![vertex_layout],
},
fragment: Some(FragmentState {
shader: UI_SLICER_SHADER_HANDLE,
shader_defs,
entry_point: "fragment".into(),
targets: vec![Some(ColorTargetState {
format: if key.hdr {
ViewTarget::TEXTURE_FORMAT_HDR
} else {
TextureFormat::bevy_default()
},
blend: Some(BlendState::ALPHA_BLENDING),
write_mask: ColorWrites::ALL,
})],
}),
layout: vec![self.view_layout.clone(), self.image_layout.clone()],
push_constant_ranges: Vec::new(),
primitive: PrimitiveState {
front_face: FrontFace::Ccw,
cull_mode: None,
unclipped_depth: false,
polygon_mode: PolygonMode::Fill,
conservative: false,
topology: PrimitiveTopology::TriangleList,
strip_index_format: None,
},
depth_stencil: None,
multisample: MultisampleState {
count: 1,
mask: !0,
alpha_to_coverage_enabled: false,
},
label: Some("ui_texture_slice_pipeline".into()),
zero_initialize_workgroup_memory: false,
}
}
}
pub struct ExtractedUiTextureSlice {
pub stack_index: u32,
pub transform: Mat4,
pub rect: Rect,
pub atlas_rect: Option<Rect>,
pub image: AssetId<Image>,
pub clip: Option<Rect>,
pub extracted_camera_entity: Entity,
pub color: LinearRgba,
pub image_scale_mode: SpriteImageMode,
pub flip_x: bool,
pub flip_y: bool,
pub inverse_scale_factor: f32,
pub main_entity: MainEntity,
pub render_entity: Entity,
}
#[derive(Resource, Default)]
pub struct ExtractedUiTextureSlices {
pub slices: Vec<ExtractedUiTextureSlice>,
}
pub fn extract_ui_texture_slices(
mut commands: Commands,
mut extracted_ui_slicers: ResMut<ExtractedUiTextureSlices>,
texture_atlases: Extract<Res<Assets<TextureAtlasLayout>>>,
slicers_query: Extract<
Query<(
Entity,
&ComputedNode,
&GlobalTransform,
&InheritedVisibility,
Option<&CalculatedClip>,
&ComputedNodeTarget,
&ImageNode,
)>,
>,
camera_map: Extract<UiCameraMap>,
) {
let mut camera_mapper = camera_map.get_mapper();
for (entity, uinode, transform, inherited_visibility, clip, camera, image) in &slicers_query {
// Skip invisible images
if !inherited_visibility.get()
|| image.color.is_fully_transparent()
|| image.image.id() == TRANSPARENT_IMAGE_HANDLE.id()
{
continue;
}
let image_scale_mode = match image.image_mode.clone() {
widget::NodeImageMode::Sliced(texture_slicer) => {
SpriteImageMode::Sliced(texture_slicer)
}
widget::NodeImageMode::Tiled {
tile_x,
tile_y,
stretch_value,
} => SpriteImageMode::Tiled {
tile_x,
tile_y,
stretch_value,
},
_ => continue,
};
let Some(extracted_camera_entity) = camera_mapper.map(camera) else {
continue;
};
let atlas_rect = image
.texture_atlas
.as_ref()
.and_then(|s| s.texture_rect(&texture_atlases))
.map(|r| r.as_rect());
let atlas_rect = match (atlas_rect, image.rect) {
(None, None) => None,
(None, Some(image_rect)) => Some(image_rect),
(Some(atlas_rect), None) => Some(atlas_rect),
(Some(atlas_rect), Some(mut image_rect)) => {
image_rect.min += atlas_rect.min;
image_rect.max += atlas_rect.min;
Some(image_rect)
}
};
extracted_ui_slicers.slices.push(ExtractedUiTextureSlice {
render_entity: commands.spawn(TemporaryRenderEntity).id(),
stack_index: uinode.stack_index,
transform: transform.compute_matrix(),
color: image.color.into(),
rect: Rect {
min: Vec2::ZERO,
max: uinode.size,
},
clip: clip.map(|clip| clip.clip),
image: image.image.id(),
extracted_camera_entity,
image_scale_mode,
atlas_rect,
flip_x: image.flip_x,
flip_y: image.flip_y,
inverse_scale_factor: uinode.inverse_scale_factor,
main_entity: entity.into(),
});
}
}
#[expect(
clippy::too_many_arguments,
reason = "it's a system that needs a lot of them"
)]
pub fn queue_ui_slices(
extracted_ui_slicers: ResMut<ExtractedUiTextureSlices>,
ui_slicer_pipeline: Res<UiTextureSlicePipeline>,
mut pipelines: ResMut<SpecializedRenderPipelines<UiTextureSlicePipeline>>,
mut transparent_render_phases: ResMut<ViewSortedRenderPhases<TransparentUi>>,
mut render_views: Query<&UiCameraView, With<ExtractedView>>,
camera_views: Query<&ExtractedView>,
pipeline_cache: Res<PipelineCache>,
draw_functions: Res<DrawFunctions<TransparentUi>>,
) {
let draw_function = draw_functions.read().id::<DrawUiTextureSlices>();
for (index, extracted_slicer) in extracted_ui_slicers.slices.iter().enumerate() {
let Ok(default_camera_view) =
render_views.get_mut(extracted_slicer.extracted_camera_entity)
else {
continue;
};
let Ok(view) = camera_views.get(default_camera_view.0) else {
continue;
};
let Some(transparent_phase) = transparent_render_phases.get_mut(&view.retained_view_entity)
else {
continue;
};
let pipeline = pipelines.specialize(
&pipeline_cache,
&ui_slicer_pipeline,
UiTextureSlicePipelineKey { hdr: view.hdr },
);
transparent_phase.add(TransparentUi {
draw_function,
pipeline,
entity: (extracted_slicer.render_entity, extracted_slicer.main_entity),
sort_key: FloatOrd(
extracted_slicer.stack_index as f32 + stack_z_offsets::TEXTURE_SLICE,
),
batch_range: 0..0,
extra_index: PhaseItemExtraIndex::None,
index,
indexed: true,
});
}
}
pub fn prepare_ui_slices(
mut commands: Commands,
render_device: Res<RenderDevice>,
render_queue: Res<RenderQueue>,
mut ui_meta: ResMut<UiTextureSliceMeta>,
mut extracted_slices: ResMut<ExtractedUiTextureSlices>,
view_uniforms: Res<ViewUniforms>,
texture_slicer_pipeline: Res<UiTextureSlicePipeline>,
mut image_bind_groups: ResMut<UiTextureSliceImageBindGroups>,
gpu_images: Res<RenderAssets<GpuImage>>,
mut phases: ResMut<ViewSortedRenderPhases<TransparentUi>>,
events: Res<SpriteAssetEvents>,
mut previous_len: Local<usize>,
) {
// If an image has changed, the GpuImage has (probably) changed
for event in &events.images {
match event {
AssetEvent::Added { .. } |
AssetEvent::Unused { .. } |
// Images don't have dependencies
AssetEvent::LoadedWithDependencies { .. } => {}
AssetEvent::Modified { id } | AssetEvent::Removed { id } => {
image_bind_groups.values.remove(id);
}
};
}
if let Some(view_binding) = view_uniforms.uniforms.binding() {
let mut batches: Vec<(Entity, UiTextureSlicerBatch)> = Vec::with_capacity(*previous_len);
ui_meta.vertices.clear();
ui_meta.indices.clear();
ui_meta.view_bind_group = Some(render_device.create_bind_group(
"ui_texture_slice_view_bind_group",
&texture_slicer_pipeline.view_layout,
&BindGroupEntries::single(view_binding),
));
// Buffer indexes
let mut vertices_index = 0;
let mut indices_index = 0;
for ui_phase in phases.values_mut() {
let mut batch_item_index = 0;
let mut batch_image_handle = AssetId::invalid();
let mut batch_image_size = Vec2::ZERO;
for item_index in 0..ui_phase.items.len() {
let item = &mut ui_phase.items[item_index];
if let Some(texture_slices) = extracted_slices
.slices
.get(item.index)
.filter(|n| item.entity() == n.render_entity)
{
let mut existing_batch = batches.last_mut();
if batch_image_handle == AssetId::invalid()
|| existing_batch.is_none()
|| (batch_image_handle != AssetId::default()
&& texture_slices.image != AssetId::default()
&& batch_image_handle != texture_slices.image)
{
if let Some(gpu_image) = gpu_images.get(texture_slices.image) {
batch_item_index = item_index;
batch_image_handle = texture_slices.image;
batch_image_size = gpu_image.size_2d().as_vec2();
let new_batch = UiTextureSlicerBatch {
range: vertices_index..vertices_index,
image: texture_slices.image,
};
batches.push((item.entity(), new_batch));
image_bind_groups
.values
.entry(batch_image_handle)
.or_insert_with(|| {
render_device.create_bind_group(
"ui_texture_slice_image_layout",
&texture_slicer_pipeline.image_layout,
&BindGroupEntries::sequential((
&gpu_image.texture_view,
&gpu_image.sampler,
)),
)
});
existing_batch = batches.last_mut();
} else {
continue;
}
} else if batch_image_handle == AssetId::default()
&& texture_slices.image != AssetId::default()
{
if let Some(gpu_image) = gpu_images.get(texture_slices.image) {
batch_image_handle = texture_slices.image;
batch_image_size = gpu_image.size_2d().as_vec2();
existing_batch.as_mut().unwrap().1.image = texture_slices.image;
image_bind_groups
.values
.entry(batch_image_handle)
.or_insert_with(|| {
render_device.create_bind_group(
"ui_texture_slice_image_layout",
&texture_slicer_pipeline.image_layout,
&BindGroupEntries::sequential((
&gpu_image.texture_view,
&gpu_image.sampler,
)),
)
});
} else {
continue;
}
}
let uinode_rect = texture_slices.rect;
let rect_size = uinode_rect.size().extend(1.0);
// Specify the corners of the node
let positions = QUAD_VERTEX_POSITIONS
.map(|pos| (texture_slices.transform * (pos * rect_size).extend(1.)).xyz());
// Calculate the effect of clipping
// Note: this won't work with rotation/scaling, but that's much more complex (may need more that 2 quads)
let positions_diff = if let Some(clip) = texture_slices.clip {
[
Vec2::new(
f32::max(clip.min.x - positions[0].x, 0.),
f32::max(clip.min.y - positions[0].y, 0.),
),
Vec2::new(
f32::min(clip.max.x - positions[1].x, 0.),
f32::max(clip.min.y - positions[1].y, 0.),
),
Vec2::new(
f32::min(clip.max.x - positions[2].x, 0.),
f32::min(clip.max.y - positions[2].y, 0.),
),
Vec2::new(
f32::max(clip.min.x - positions[3].x, 0.),
f32::min(clip.max.y - positions[3].y, 0.),
),
]
} else {
[Vec2::ZERO; 4]
};
let positions_clipped = [
positions[0] + positions_diff[0].extend(0.),
positions[1] + positions_diff[1].extend(0.),
positions[2] + positions_diff[2].extend(0.),
positions[3] + positions_diff[3].extend(0.),
];
let transformed_rect_size =
texture_slices.transform.transform_vector3(rect_size);
// Don't try to cull nodes that have a rotation
// In a rotation around the Z-axis, this value is 0.0 for an angle of 0.0 or π
// In those two cases, the culling check can proceed normally as corners will be on
// horizontal / vertical lines
// For all other angles, bypass the culling check
// This does not properly handles all rotations on all axis
if texture_slices.transform.x_axis[1] == 0.0 {
// Cull nodes that are completely clipped
if positions_diff[0].x - positions_diff[1].x >= transformed_rect_size.x
|| positions_diff[1].y - positions_diff[2].y >= transformed_rect_size.y
{
continue;
}
}
let flags = if texture_slices.image != AssetId::default() {
shader_flags::TEXTURED
} else {
shader_flags::UNTEXTURED
};
let uvs = if flags == shader_flags::UNTEXTURED {
[Vec2::ZERO, Vec2::X, Vec2::ONE, Vec2::Y]
} else {
let atlas_extent = uinode_rect.max;
[
Vec2::new(
uinode_rect.min.x + positions_diff[0].x,
uinode_rect.min.y + positions_diff[0].y,
),
Vec2::new(
uinode_rect.max.x + positions_diff[1].x,
uinode_rect.min.y + positions_diff[1].y,
),
Vec2::new(
uinode_rect.max.x + positions_diff[2].x,
uinode_rect.max.y + positions_diff[2].y,
),
Vec2::new(
uinode_rect.min.x + positions_diff[3].x,
uinode_rect.max.y + positions_diff[3].y,
),
]
.map(|pos| pos / atlas_extent)
};
let color = texture_slices.color.to_f32_array();
let (image_size, mut atlas) = if let Some(atlas) = texture_slices.atlas_rect {
(
atlas.size(),
[
atlas.min.x / batch_image_size.x,
atlas.min.y / batch_image_size.y,
atlas.max.x / batch_image_size.x,
atlas.max.y / batch_image_size.y,
],
)
} else {
(batch_image_size, [0., 0., 1., 1.])
};
if texture_slices.flip_x {
atlas.swap(0, 2);
}
if texture_slices.flip_y {
atlas.swap(1, 3);
}
let [slices, border, repeat] = compute_texture_slices(
image_size,
uinode_rect.size() * texture_slices.inverse_scale_factor,
&texture_slices.image_scale_mode,
);
for i in 0..4 {
ui_meta.vertices.push(UiTextureSliceVertex {
position: positions_clipped[i].into(),
uv: uvs[i].into(),
color,
slices,
border,
repeat,
atlas,
});
}
for &i in &QUAD_INDICES {
ui_meta.indices.push(indices_index + i as u32);
}
vertices_index += 6;
indices_index += 4;
existing_batch.unwrap().1.range.end = vertices_index;
ui_phase.items[batch_item_index].batch_range_mut().end += 1;
} else {
batch_image_handle = AssetId::invalid();
}
}
}
ui_meta.vertices.write_buffer(&render_device, &render_queue);
ui_meta.indices.write_buffer(&render_device, &render_queue);
*previous_len = batches.len();
commands.try_insert_batch(batches);
}
extracted_slices.slices.clear();
}
pub type DrawUiTextureSlices = (
SetItemPipeline,
SetSlicerViewBindGroup<0>,
SetSlicerTextureBindGroup<1>,
DrawSlicer,
);
pub struct SetSlicerViewBindGroup<const I: usize>;
impl<P: PhaseItem, const I: usize> RenderCommand<P> for SetSlicerViewBindGroup<I> {
type Param = SRes<UiTextureSliceMeta>;
type ViewQuery = Read<ViewUniformOffset>;
type ItemQuery = ();
fn render<'w>(
_item: &P,
view_uniform: &'w ViewUniformOffset,
_entity: Option<()>,
ui_meta: SystemParamItem<'w, '_, Self::Param>,
pass: &mut TrackedRenderPass<'w>,
) -> RenderCommandResult {
let Some(view_bind_group) = ui_meta.into_inner().view_bind_group.as_ref() else {
return RenderCommandResult::Failure("view_bind_group not available");
};
pass.set_bind_group(I, view_bind_group, &[view_uniform.offset]);
RenderCommandResult::Success
}
}
pub struct SetSlicerTextureBindGroup<const I: usize>;
impl<P: PhaseItem, const I: usize> RenderCommand<P> for SetSlicerTextureBindGroup<I> {
type Param = SRes<UiTextureSliceImageBindGroups>;
type ViewQuery = ();
type ItemQuery = Read<UiTextureSlicerBatch>;
#[inline]
fn render<'w>(
_item: &P,
_view: (),
batch: Option<&'w UiTextureSlicerBatch>,
image_bind_groups: SystemParamItem<'w, '_, Self::Param>,
pass: &mut TrackedRenderPass<'w>,
) -> RenderCommandResult {
let image_bind_groups = image_bind_groups.into_inner();
let Some(batch) = batch else {
return RenderCommandResult::Skip;
};
pass.set_bind_group(I, image_bind_groups.values.get(&batch.image).unwrap(), &[]);
RenderCommandResult::Success
}
}
pub struct DrawSlicer;
impl<P: PhaseItem> RenderCommand<P> for DrawSlicer {
type Param = SRes<UiTextureSliceMeta>;
type ViewQuery = ();
type ItemQuery = Read<UiTextureSlicerBatch>;
#[inline]
fn render<'w>(
_item: &P,
_view: (),
batch: Option<&'w UiTextureSlicerBatch>,
ui_meta: SystemParamItem<'w, '_, Self::Param>,
pass: &mut TrackedRenderPass<'w>,
) -> RenderCommandResult {
let Some(batch) = batch else {
return RenderCommandResult::Skip;
};
let ui_meta = ui_meta.into_inner();
let Some(vertices) = ui_meta.vertices.buffer() else {
return RenderCommandResult::Failure("missing vertices to draw ui");
};
let Some(indices) = ui_meta.indices.buffer() else {
return RenderCommandResult::Failure("missing indices to draw ui");
};
// Store the vertices
pass.set_vertex_buffer(0, vertices.slice(..));
// Define how to "connect" the vertices
pass.set_index_buffer(indices.slice(..), 0, IndexFormat::Uint32);
// Draw the vertices
pass.draw_indexed(batch.range.clone(), 0, 0..1);
RenderCommandResult::Success
}
}
fn compute_texture_slices(
image_size: Vec2,
target_size: Vec2,
image_scale_mode: &SpriteImageMode,
) -> [[f32; 4]; 3] {
match image_scale_mode {
SpriteImageMode::Sliced(TextureSlicer {
border: border_rect,
center_scale_mode,
sides_scale_mode,
max_corner_scale,
}) => {
let min_coeff = (target_size / image_size)
.min_element()
.min(*max_corner_scale);
// calculate the normalized extents of the nine-patched image slices
let slices = [
border_rect.left / image_size.x,
border_rect.top / image_size.y,
1. - border_rect.right / image_size.x,
1. - border_rect.bottom / image_size.y,
];
// calculate the normalized extents of the target slices
let border = [
(border_rect.left / target_size.x) * min_coeff,
(border_rect.top / target_size.y) * min_coeff,
1. - (border_rect.right / target_size.x) * min_coeff,
1. - (border_rect.bottom / target_size.y) * min_coeff,
];
let image_side_width = image_size.x * (slices[2] - slices[0]);
let image_side_height = image_size.y * (slices[3] - slices[1]);
let target_side_width = target_size.x * (border[2] - border[0]);
let target_side_height = target_size.y * (border[3] - border[1]);
// compute the number of times to repeat the side and center slices when tiling along each axis
// if the returned value is `1.` the slice will be stretched to fill the axis.
let repeat_side_x =
compute_tiled_subaxis(image_side_width, target_side_width, sides_scale_mode);
let repeat_side_y =
compute_tiled_subaxis(image_side_height, target_side_height, sides_scale_mode);
let repeat_center_x =
compute_tiled_subaxis(image_side_width, target_side_width, center_scale_mode);
let repeat_center_y =
compute_tiled_subaxis(image_side_height, target_side_height, center_scale_mode);
[
slices,
border,
[
repeat_side_x,
repeat_side_y,
repeat_center_x,
repeat_center_y,
],
]
}
SpriteImageMode::Tiled {
tile_x,
tile_y,
stretch_value,
} => {
let rx = compute_tiled_axis(*tile_x, image_size.x, target_size.x, *stretch_value);
let ry = compute_tiled_axis(*tile_y, image_size.y, target_size.y, *stretch_value);
[[0., 0., 1., 1.], [0., 0., 1., 1.], [1., 1., rx, ry]]
}
SpriteImageMode::Auto => {
unreachable!("Slices can not be computed for SpriteImageMode::Stretch")
}
SpriteImageMode::Scale(_) => {
unreachable!("Slices can not be computed for SpriteImageMode::Scale")
}
}
}
fn compute_tiled_axis(tile: bool, image_extent: f32, target_extent: f32, stretch: f32) -> f32 {
if tile {
let s = image_extent * stretch;
target_extent / s
} else {
1.
}
}
fn compute_tiled_subaxis(image_extent: f32, target_extent: f32, mode: &SliceScaleMode) -> f32 {
match mode {
SliceScaleMode::Stretch => 1.,
SliceScaleMode::Tile { stretch_value } => {
let s = image_extent * *stretch_value;
target_extent / s
}
}
}

View File

@@ -0,0 +1,13 @@
#define_import_path bevy_ui::ui_vertex_output
// The Vertex output of the default vertex shader for the Ui Material pipeline.
struct UiVertexOutput {
@location(0) uv: vec2<f32>,
// The size of the borders in UV space. Order is Left, Right, Top, Bottom.
@location(1) border_widths: vec4<f32>,
// The size of the borders in pixels. Order is top left, top right, bottom right, bottom left.
@location(2) border_radius: vec4<f32>,
// The size of the node in pixels. Order is width, height.
@location(3) @interpolate(flat) size: vec2<f32>,
@builtin(position) position: vec4<f32>,
};

309
vendor/bevy_ui/src/stack.rs vendored Normal file
View File

@@ -0,0 +1,309 @@
//! This module contains the systems that update the stored UI nodes stack
use bevy_ecs::prelude::*;
use bevy_platform::collections::HashSet;
use crate::{
experimental::{UiChildren, UiRootNodes},
ComputedNode, GlobalZIndex, ZIndex,
};
/// The current UI stack, which contains all UI nodes ordered by their depth (back-to-front).
///
/// The first entry is the furthest node from the camera and is the first one to get rendered
/// while the last entry is the first node to receive interactions.
#[derive(Debug, Resource, Default)]
pub struct UiStack {
/// List of UI nodes ordered from back-to-front
pub uinodes: Vec<Entity>,
}
#[derive(Default)]
pub(crate) struct ChildBufferCache {
pub inner: Vec<Vec<(Entity, i32)>>,
}
impl ChildBufferCache {
fn pop(&mut self) -> Vec<(Entity, i32)> {
self.inner.pop().unwrap_or_default()
}
fn push(&mut self, vec: Vec<(Entity, i32)>) {
self.inner.push(vec);
}
}
/// Generates the render stack for UI nodes.
///
/// Create a list of root nodes from parentless entities and entities with a `GlobalZIndex` component.
/// Then build the `UiStack` from a walk of the existing layout trees starting from each root node,
/// filtering branches by `Without<GlobalZIndex>`so that we don't revisit nodes.
pub fn ui_stack_system(
mut cache: Local<ChildBufferCache>,
mut root_nodes: Local<Vec<(Entity, (i32, i32))>>,
mut visited_root_nodes: Local<HashSet<Entity>>,
mut ui_stack: ResMut<UiStack>,
ui_root_nodes: UiRootNodes,
root_node_query: Query<(Entity, Option<&GlobalZIndex>, Option<&ZIndex>)>,
zindex_global_node_query: Query<(Entity, &GlobalZIndex, Option<&ZIndex>), With<ComputedNode>>,
ui_children: UiChildren,
zindex_query: Query<Option<&ZIndex>, (With<ComputedNode>, Without<GlobalZIndex>)>,
mut update_query: Query<&mut ComputedNode>,
) {
ui_stack.uinodes.clear();
visited_root_nodes.clear();
for (id, maybe_global_zindex, maybe_zindex) in root_node_query.iter_many(ui_root_nodes.iter()) {
root_nodes.push((
id,
(
maybe_global_zindex.map(|zindex| zindex.0).unwrap_or(0),
maybe_zindex.map(|zindex| zindex.0).unwrap_or(0),
),
));
visited_root_nodes.insert(id);
}
for (id, global_zindex, maybe_zindex) in zindex_global_node_query.iter() {
if visited_root_nodes.contains(&id) {
continue;
}
root_nodes.push((
id,
(
global_zindex.0,
maybe_zindex.map(|zindex| zindex.0).unwrap_or(0),
),
));
}
root_nodes.sort_by_key(|(_, z)| *z);
for (root_entity, _) in root_nodes.drain(..) {
update_uistack_recursive(
&mut cache,
root_entity,
&ui_children,
&zindex_query,
&mut ui_stack.uinodes,
);
}
for (i, entity) in ui_stack.uinodes.iter().enumerate() {
if let Ok(mut node) = update_query.get_mut(*entity) {
node.bypass_change_detection().stack_index = i as u32;
}
}
}
fn update_uistack_recursive(
cache: &mut ChildBufferCache,
node_entity: Entity,
ui_children: &UiChildren,
zindex_query: &Query<Option<&ZIndex>, (With<ComputedNode>, Without<GlobalZIndex>)>,
ui_stack: &mut Vec<Entity>,
) {
ui_stack.push(node_entity);
let mut child_buffer = cache.pop();
child_buffer.extend(
ui_children
.iter_ui_children(node_entity)
.filter_map(|child_entity| {
zindex_query
.get(child_entity)
.ok()
.map(|zindex| (child_entity, zindex.map(|zindex| zindex.0).unwrap_or(0)))
}),
);
child_buffer.sort_by_key(|k| k.1);
for (child_entity, _) in child_buffer.drain(..) {
update_uistack_recursive(cache, child_entity, ui_children, zindex_query, ui_stack);
}
cache.push(child_buffer);
}
#[cfg(test)]
mod tests {
use bevy_ecs::{
component::Component,
schedule::Schedule,
system::Commands,
world::{CommandQueue, World},
};
use crate::{GlobalZIndex, Node, UiStack, ZIndex};
use super::ui_stack_system;
#[derive(Component, PartialEq, Debug, Clone)]
struct Label(&'static str);
fn node_with_global_and_local_zindex(
name: &'static str,
global_zindex: i32,
local_zindex: i32,
) -> (Label, Node, GlobalZIndex, ZIndex) {
(
Label(name),
Node::default(),
GlobalZIndex(global_zindex),
ZIndex(local_zindex),
)
}
fn node_with_global_zindex(
name: &'static str,
global_zindex: i32,
) -> (Label, Node, GlobalZIndex) {
(Label(name), Node::default(), GlobalZIndex(global_zindex))
}
fn node_with_zindex(name: &'static str, zindex: i32) -> (Label, Node, ZIndex) {
(Label(name), Node::default(), ZIndex(zindex))
}
fn node_without_zindex(name: &'static str) -> (Label, Node) {
(Label(name), Node::default())
}
/// Tests the UI Stack system.
///
/// This tests for siblings default ordering according to their insertion order, but it
/// can't test the same thing for UI roots. UI roots having no parents, they do not have
/// a stable ordering that we can test against. If we test it, it may pass now and start
/// failing randomly in the future because of some unrelated `bevy_ecs` change.
#[test]
fn test_ui_stack_system() {
let mut world = World::default();
world.init_resource::<UiStack>();
let mut queue = CommandQueue::default();
let mut commands = Commands::new(&mut queue, &world);
commands.spawn(node_with_global_zindex("0", 2));
commands
.spawn(node_with_zindex("1", 1))
.with_children(|parent| {
parent
.spawn(node_without_zindex("1-0"))
.with_children(|parent| {
parent.spawn(node_without_zindex("1-0-0"));
parent.spawn(node_without_zindex("1-0-1"));
parent.spawn(node_with_zindex("1-0-2", -1));
});
parent.spawn(node_without_zindex("1-1"));
parent
.spawn(node_with_global_zindex("1-2", -1))
.with_children(|parent| {
parent.spawn(node_without_zindex("1-2-0"));
parent.spawn(node_with_global_zindex("1-2-1", -3));
parent
.spawn(node_without_zindex("1-2-2"))
.with_children(|_| ());
parent.spawn(node_without_zindex("1-2-3"));
});
parent.spawn(node_without_zindex("1-3"));
});
commands
.spawn(node_without_zindex("2"))
.with_children(|parent| {
parent
.spawn(node_without_zindex("2-0"))
.with_children(|_parent| ());
parent
.spawn(node_without_zindex("2-1"))
.with_children(|parent| {
parent.spawn(node_without_zindex("2-1-0"));
});
});
commands.spawn(node_with_global_zindex("3", -2));
queue.apply(&mut world);
let mut schedule = Schedule::default();
schedule.add_systems(ui_stack_system);
schedule.run(&mut world);
let mut query = world.query::<&Label>();
let ui_stack = world.resource::<UiStack>();
let actual_result = ui_stack
.uinodes
.iter()
.map(|entity| query.get(&world, *entity).unwrap().clone())
.collect::<Vec<_>>();
let expected_result = vec![
(Label("1-2-1")), // GlobalZIndex(-3)
(Label("3")), // GlobalZIndex(-2)
(Label("1-2")), // GlobalZIndex(-1)
(Label("1-2-0")),
(Label("1-2-2")),
(Label("1-2-3")),
(Label("2")),
(Label("2-0")),
(Label("2-1")),
(Label("2-1-0")),
(Label("1")), // ZIndex(1)
(Label("1-0")),
(Label("1-0-2")), // ZIndex(-1)
(Label("1-0-0")),
(Label("1-0-1")),
(Label("1-1")),
(Label("1-3")),
(Label("0")), // GlobalZIndex(2)
];
assert_eq!(actual_result, expected_result);
}
#[test]
fn test_with_equal_global_zindex_zindex_decides_order() {
let mut world = World::default();
world.init_resource::<UiStack>();
let mut queue = CommandQueue::default();
let mut commands = Commands::new(&mut queue, &world);
commands.spawn(node_with_global_and_local_zindex("0", -1, 1));
commands.spawn(node_with_global_and_local_zindex("1", -1, 2));
commands.spawn(node_with_global_and_local_zindex("2", 1, 3));
commands.spawn(node_with_global_and_local_zindex("3", 1, -3));
commands
.spawn(node_without_zindex("4"))
.with_children(|builder| {
builder.spawn(node_with_global_and_local_zindex("5", 0, -1));
builder.spawn(node_with_global_and_local_zindex("6", 0, 1));
builder.spawn(node_with_global_and_local_zindex("7", -1, -1));
builder.spawn(node_with_global_zindex("8", 1));
});
queue.apply(&mut world);
let mut schedule = Schedule::default();
schedule.add_systems(ui_stack_system);
schedule.run(&mut world);
let mut query = world.query::<&Label>();
let ui_stack = world.resource::<UiStack>();
let actual_result = ui_stack
.uinodes
.iter()
.map(|entity| query.get(&world, *entity).unwrap().clone())
.collect::<Vec<_>>();
let expected_result = vec![
(Label("7")),
(Label("0")),
(Label("1")),
(Label("5")),
(Label("4")),
(Label("6")),
(Label("3")),
(Label("8")),
(Label("2")),
];
assert_eq!(actual_result, expected_result);
}
}

182
vendor/bevy_ui/src/ui_material.rs vendored Normal file
View File

@@ -0,0 +1,182 @@
use crate::Node;
use bevy_asset::{Asset, AssetId, Handle};
use bevy_derive::{Deref, DerefMut};
use bevy_ecs::{component::Component, reflect::ReflectComponent};
use bevy_reflect::{prelude::ReflectDefault, Reflect};
use bevy_render::{
extract_component::ExtractComponent,
render_resource::{AsBindGroup, RenderPipelineDescriptor, ShaderRef},
};
use core::hash::Hash;
use derive_more::derive::From;
/// Materials are used alongside [`UiMaterialPlugin`](crate::UiMaterialPlugin) and [`MaterialNode`]
/// to spawn entities that are rendered with a specific [`UiMaterial`] type. They serve as an easy to use high level
/// way to render `Node` entities with custom shader logic.
///
/// `UiMaterials` must implement [`AsBindGroup`] to define how data will be transferred to the GPU and bound in shaders.
/// [`AsBindGroup`] can be derived, which makes generating bindings straightforward. See the [`AsBindGroup`] docs for details.
///
/// Materials must also implement [`Asset`] so they can be treated as such.
///
/// If you are only using the fragment shader, make sure your shader imports the `UiVertexOutput`
/// from `bevy_ui::ui_vertex_output` and uses it as the input of your fragment shader like the
/// example below does.
///
/// # Example
///
/// Here is a simple [`UiMaterial`] implementation. The [`AsBindGroup`] derive has many features. To see what else is available,
/// check out the [`AsBindGroup`] documentation.
/// ```
/// # use bevy_ui::prelude::*;
/// # use bevy_ecs::prelude::*;
/// # use bevy_image::Image;
/// # use bevy_reflect::TypePath;
/// # use bevy_render::render_resource::{AsBindGroup, ShaderRef};
/// # use bevy_color::LinearRgba;
/// # use bevy_asset::{Handle, AssetServer, Assets, Asset};
///
/// #[derive(AsBindGroup, Asset, TypePath, Debug, Clone)]
/// pub struct CustomMaterial {
/// // Uniform bindings must implement `ShaderType`, which will be used to convert the value to
/// // its shader-compatible equivalent. Most core math types already implement `ShaderType`.
/// #[uniform(0)]
/// color: LinearRgba,
/// // Images can be bound as textures in shaders. If the Image's sampler is also needed, just
/// // add the sampler attribute with a different binding index.
/// #[texture(1)]
/// #[sampler(2)]
/// color_texture: Handle<Image>,
/// }
///
/// // All functions on `UiMaterial` have default impls. You only need to implement the
/// // functions that are relevant for your material.
/// impl UiMaterial for CustomMaterial {
/// fn fragment_shader() -> ShaderRef {
/// "shaders/custom_material.wgsl".into()
/// }
/// }
///
/// // Spawn an entity using `CustomMaterial`.
/// fn setup(mut commands: Commands, mut materials: ResMut<Assets<CustomMaterial>>, asset_server: Res<AssetServer>) {
/// commands.spawn((
/// MaterialNode(materials.add(CustomMaterial {
/// color: LinearRgba::RED,
/// color_texture: asset_server.load("some_image.png"),
/// })),
/// Node {
/// width: Val::Percent(100.0),
/// ..Default::default()
/// },
/// ));
/// }
/// ```
/// In WGSL shaders, the material's binding would look like this:
///
/// If you only use the fragment shader make sure to import `UiVertexOutput` from
/// `bevy_ui::ui_vertex_output` in your wgsl shader.
/// Also note that bind group 0 is always bound to the [`View Uniform`](bevy_render::view::ViewUniform)
/// and the [`Globals Uniform`](bevy_render::globals::GlobalsUniform).
///
/// ```wgsl
/// #import bevy_ui::ui_vertex_output UiVertexOutput
///
/// struct CustomMaterial {
/// color: vec4<f32>,
/// }
///
/// @group(1) @binding(0)
/// var<uniform> material: CustomMaterial;
/// @group(1) @binding(1)
/// var color_texture: texture_2d<f32>;
/// @group(1) @binding(2)
/// var color_sampler: sampler;
///
/// @fragment
/// fn fragment(in: UiVertexOutput) -> @location(0) vec4<f32> {
///
/// }
/// ```
pub trait UiMaterial: AsBindGroup + Asset + Clone + Sized {
/// Returns this materials vertex shader. If [`ShaderRef::Default`] is returned, the default UI
/// vertex shader will be used.
fn vertex_shader() -> ShaderRef {
ShaderRef::Default
}
/// Returns this materials fragment shader. If [`ShaderRef::Default`] is returned, the default
/// UI fragment shader will be used.
fn fragment_shader() -> ShaderRef {
ShaderRef::Default
}
#[expect(
unused_variables,
reason = "The parameters here are intentionally unused by the default implementation; however, putting underscores here will result in the underscores being copied by rust-analyzer's tab completion."
)]
#[inline]
fn specialize(descriptor: &mut RenderPipelineDescriptor, key: UiMaterialKey<Self>) {}
}
pub struct UiMaterialKey<M: UiMaterial> {
pub hdr: bool,
pub bind_group_data: M::Data,
}
impl<M: UiMaterial> Eq for UiMaterialKey<M> where M::Data: PartialEq {}
impl<M: UiMaterial> PartialEq for UiMaterialKey<M>
where
M::Data: PartialEq,
{
fn eq(&self, other: &Self) -> bool {
self.hdr == other.hdr && self.bind_group_data == other.bind_group_data
}
}
impl<M: UiMaterial> Clone for UiMaterialKey<M>
where
M::Data: Clone,
{
fn clone(&self) -> Self {
Self {
hdr: self.hdr,
bind_group_data: self.bind_group_data.clone(),
}
}
}
impl<M: UiMaterial> Hash for UiMaterialKey<M>
where
M::Data: Hash,
{
fn hash<H: core::hash::Hasher>(&self, state: &mut H) {
self.hdr.hash(state);
self.bind_group_data.hash(state);
}
}
#[derive(
Component, Clone, Debug, Deref, DerefMut, Reflect, PartialEq, Eq, ExtractComponent, From,
)]
#[reflect(Component, Default)]
#[require(Node)]
pub struct MaterialNode<M: UiMaterial>(pub Handle<M>);
impl<M: UiMaterial> Default for MaterialNode<M> {
fn default() -> Self {
Self(Handle::default())
}
}
impl<M: UiMaterial> From<MaterialNode<M>> for AssetId<M> {
fn from(material: MaterialNode<M>) -> Self {
material.id()
}
}
impl<M: UiMaterial> From<&MaterialNode<M>> for AssetId<M> {
fn from(material: &MaterialNode<M>) -> Self {
material.id()
}
}

2830
vendor/bevy_ui/src/ui_node.rs vendored Normal file

File diff suppressed because it is too large Load Diff

635
vendor/bevy_ui/src/update.rs vendored Normal file
View File

@@ -0,0 +1,635 @@
//! This module contains systems that update the UI when something changes
use crate::{
experimental::{UiChildren, UiRootNodes},
CalculatedClip, ComputedNodeTarget, DefaultUiCamera, Display, Node, OverflowAxis, UiScale,
UiTargetCamera,
};
use super::ComputedNode;
use bevy_ecs::{
change_detection::DetectChangesMut,
entity::Entity,
hierarchy::ChildOf,
query::{Changed, With},
system::{Commands, Query, Res},
};
use bevy_math::{Rect, UVec2};
use bevy_render::camera::Camera;
use bevy_sprite::BorderRect;
use bevy_transform::components::GlobalTransform;
/// Updates clipping for all nodes
pub fn update_clipping_system(
mut commands: Commands,
root_nodes: UiRootNodes,
mut node_query: Query<(
&Node,
&ComputedNode,
&GlobalTransform,
Option<&mut CalculatedClip>,
)>,
ui_children: UiChildren,
) {
for root_node in root_nodes.iter() {
update_clipping(
&mut commands,
&ui_children,
&mut node_query,
root_node,
None,
);
}
}
fn update_clipping(
commands: &mut Commands,
ui_children: &UiChildren,
node_query: &mut Query<(
&Node,
&ComputedNode,
&GlobalTransform,
Option<&mut CalculatedClip>,
)>,
entity: Entity,
mut maybe_inherited_clip: Option<Rect>,
) {
let Ok((node, computed_node, global_transform, maybe_calculated_clip)) =
node_query.get_mut(entity)
else {
return;
};
// If `display` is None, clip the entire node and all its descendants by replacing the inherited clip with a default rect (which is empty)
if node.display == Display::None {
maybe_inherited_clip = Some(Rect::default());
}
// Update this node's CalculatedClip component
if let Some(mut calculated_clip) = maybe_calculated_clip {
if let Some(inherited_clip) = maybe_inherited_clip {
// Replace the previous calculated clip with the inherited clipping rect
if calculated_clip.clip != inherited_clip {
*calculated_clip = CalculatedClip {
clip: inherited_clip,
};
}
} else {
// No inherited clipping rect, remove the component
commands.entity(entity).remove::<CalculatedClip>();
}
} else if let Some(inherited_clip) = maybe_inherited_clip {
// No previous calculated clip, add a new CalculatedClip component with the inherited clipping rect
commands.entity(entity).try_insert(CalculatedClip {
clip: inherited_clip,
});
}
// Calculate new clip rectangle for children nodes
let children_clip = if node.overflow.is_visible() {
// The current node doesn't clip, propagate the optional inherited clipping rect to any children
maybe_inherited_clip
} else {
// Find the current node's clipping rect and intersect it with the inherited clipping rect, if one exists
let mut clip_rect = Rect::from_center_size(
global_transform.translation().truncate(),
computed_node.size(),
);
// Content isn't clipped at the edges of the node but at the edges of the region specified by [`Node::overflow_clip_margin`].
//
// `clip_inset` should always fit inside `node_rect`.
// Even if `clip_inset` were to overflow, we won't return a degenerate result as `Rect::intersect` will clamp the intersection, leaving it empty.
let clip_inset = match node.overflow_clip_margin.visual_box {
crate::OverflowClipBox::BorderBox => BorderRect::ZERO,
crate::OverflowClipBox::ContentBox => computed_node.content_inset(),
crate::OverflowClipBox::PaddingBox => computed_node.border(),
};
clip_rect.min.x += clip_inset.left;
clip_rect.min.y += clip_inset.top;
clip_rect.max.x -= clip_inset.right;
clip_rect.max.y -= clip_inset.bottom;
clip_rect = clip_rect
.inflate(node.overflow_clip_margin.margin.max(0.) / computed_node.inverse_scale_factor);
if node.overflow.x == OverflowAxis::Visible {
clip_rect.min.x = -f32::INFINITY;
clip_rect.max.x = f32::INFINITY;
}
if node.overflow.y == OverflowAxis::Visible {
clip_rect.min.y = -f32::INFINITY;
clip_rect.max.y = f32::INFINITY;
}
Some(maybe_inherited_clip.map_or(clip_rect, |c| c.intersect(clip_rect)))
};
for child in ui_children.iter_ui_children(entity) {
update_clipping(commands, ui_children, node_query, child, children_clip);
}
}
pub fn update_ui_context_system(
default_ui_camera: DefaultUiCamera,
ui_scale: Res<UiScale>,
camera_query: Query<&Camera>,
target_camera_query: Query<&UiTargetCamera>,
ui_root_nodes: UiRootNodes,
mut computed_target_query: Query<&mut ComputedNodeTarget>,
ui_children: UiChildren,
reparented_nodes: Query<(Entity, &ChildOf), (Changed<ChildOf>, With<ComputedNodeTarget>)>,
) {
let default_camera_entity = default_ui_camera.get();
for root_entity in ui_root_nodes.iter() {
let camera = target_camera_query
.get(root_entity)
.ok()
.map(UiTargetCamera::entity)
.or(default_camera_entity)
.unwrap_or(Entity::PLACEHOLDER);
let (scale_factor, physical_size) = camera_query
.get(camera)
.ok()
.map(|camera| {
(
camera.target_scaling_factor().unwrap_or(1.) * ui_scale.0,
camera.physical_viewport_size().unwrap_or(UVec2::ZERO),
)
})
.unwrap_or((1., UVec2::ZERO));
update_contexts_recursively(
root_entity,
ComputedNodeTarget {
camera,
scale_factor,
physical_size,
},
&ui_children,
&mut computed_target_query,
);
}
for (entity, child_of) in reparented_nodes.iter() {
let Ok(computed_target) = computed_target_query.get(child_of.parent()) else {
continue;
};
update_contexts_recursively(
entity,
*computed_target,
&ui_children,
&mut computed_target_query,
);
}
}
fn update_contexts_recursively(
entity: Entity,
inherited_computed_target: ComputedNodeTarget,
ui_children: &UiChildren,
query: &mut Query<&mut ComputedNodeTarget>,
) {
if query
.get_mut(entity)
.map(|mut computed_target| computed_target.set_if_neq(inherited_computed_target))
.unwrap_or(false)
{
for child in ui_children.iter_ui_children(entity) {
update_contexts_recursively(child, inherited_computed_target, ui_children, query);
}
}
}
#[cfg(test)]
mod tests {
use bevy_asset::AssetEvent;
use bevy_asset::Assets;
use bevy_core_pipeline::core_2d::Camera2d;
use bevy_ecs::event::Events;
use bevy_ecs::hierarchy::ChildOf;
use bevy_ecs::schedule::IntoScheduleConfigs;
use bevy_ecs::schedule::Schedule;
use bevy_ecs::world::World;
use bevy_image::Image;
use bevy_math::UVec2;
use bevy_render::camera::Camera;
use bevy_render::camera::ManualTextureViews;
use bevy_render::camera::RenderTarget;
use bevy_utils::default;
use bevy_window::PrimaryWindow;
use bevy_window::Window;
use bevy_window::WindowCreated;
use bevy_window::WindowRef;
use bevy_window::WindowResized;
use bevy_window::WindowResolution;
use bevy_window::WindowScaleFactorChanged;
use crate::ComputedNodeTarget;
use crate::IsDefaultUiCamera;
use crate::Node;
use crate::UiScale;
use crate::UiTargetCamera;
fn setup_test_world_and_schedule() -> (World, Schedule) {
let mut world = World::new();
world.init_resource::<UiScale>();
// init resources required by `camera_system`
world.init_resource::<Events<WindowScaleFactorChanged>>();
world.init_resource::<Events<WindowResized>>();
world.init_resource::<Events<WindowCreated>>();
world.init_resource::<Events<AssetEvent<Image>>>();
world.init_resource::<Assets<Image>>();
world.init_resource::<ManualTextureViews>();
let mut schedule = Schedule::default();
schedule.add_systems(
(
bevy_render::camera::camera_system,
super::update_ui_context_system,
)
.chain(),
);
(world, schedule)
}
#[test]
fn update_context_for_single_ui_root() {
let (mut world, mut schedule) = setup_test_world_and_schedule();
let scale_factor = 10.;
let physical_size = UVec2::new(1000, 500);
world.spawn((
Window {
resolution: WindowResolution::new(physical_size.x as f32, physical_size.y as f32)
.with_scale_factor_override(10.),
..Default::default()
},
PrimaryWindow,
));
let camera = world.spawn(Camera2d).id();
let uinode = world.spawn(Node::default()).id();
schedule.run(&mut world);
assert_eq!(
*world.get::<ComputedNodeTarget>(uinode).unwrap(),
ComputedNodeTarget {
camera,
physical_size,
scale_factor,
}
);
}
#[test]
fn update_multiple_context_for_multiple_ui_roots() {
let (mut world, mut schedule) = setup_test_world_and_schedule();
let scale1 = 1.;
let size1 = UVec2::new(100, 100);
let scale2 = 2.;
let size2 = UVec2::new(200, 200);
world.spawn((
Window {
resolution: WindowResolution::new(size1.x as f32, size1.y as f32)
.with_scale_factor_override(scale1),
..Default::default()
},
PrimaryWindow,
));
let window_2 = world
.spawn((Window {
resolution: WindowResolution::new(size2.x as f32, size2.y as f32)
.with_scale_factor_override(scale2),
..Default::default()
},))
.id();
let camera1 = world.spawn((Camera2d, IsDefaultUiCamera)).id();
let camera2 = world
.spawn((
Camera2d,
Camera {
target: RenderTarget::Window(WindowRef::Entity(window_2)),
..default()
},
))
.id();
let uinode1a = world.spawn(Node::default()).id();
let uinode2a = world.spawn((Node::default(), UiTargetCamera(camera2))).id();
let uinode2b = world.spawn((Node::default(), UiTargetCamera(camera2))).id();
let uinode2c = world.spawn((Node::default(), UiTargetCamera(camera2))).id();
let uinode1b = world.spawn(Node::default()).id();
schedule.run(&mut world);
for (uinode, camera, scale_factor, physical_size) in [
(uinode1a, camera1, scale1, size1),
(uinode1b, camera1, scale1, size1),
(uinode2a, camera2, scale2, size2),
(uinode2b, camera2, scale2, size2),
(uinode2c, camera2, scale2, size2),
] {
assert_eq!(
*world.get::<ComputedNodeTarget>(uinode).unwrap(),
ComputedNodeTarget {
camera,
scale_factor,
physical_size,
}
);
}
}
#[test]
fn update_context_on_changed_camera() {
let (mut world, mut schedule) = setup_test_world_and_schedule();
let scale1 = 1.;
let size1 = UVec2::new(100, 100);
let scale2 = 2.;
let size2 = UVec2::new(200, 200);
world.spawn((
Window {
resolution: WindowResolution::new(size1.x as f32, size1.y as f32)
.with_scale_factor_override(scale1),
..Default::default()
},
PrimaryWindow,
));
let window_2 = world
.spawn((Window {
resolution: WindowResolution::new(size2.x as f32, size2.y as f32)
.with_scale_factor_override(scale2),
..Default::default()
},))
.id();
let camera1 = world.spawn((Camera2d, IsDefaultUiCamera)).id();
let camera2 = world
.spawn((
Camera2d,
Camera {
target: RenderTarget::Window(WindowRef::Entity(window_2)),
..default()
},
))
.id();
let uinode = world.spawn(Node::default()).id();
schedule.run(&mut world);
assert_eq!(
world
.get::<ComputedNodeTarget>(uinode)
.unwrap()
.scale_factor,
scale1
);
assert_eq!(
world
.get::<ComputedNodeTarget>(uinode)
.unwrap()
.physical_size,
size1
);
assert_eq!(
world
.get::<ComputedNodeTarget>(uinode)
.unwrap()
.camera()
.unwrap(),
camera1
);
world.entity_mut(uinode).insert(UiTargetCamera(camera2));
schedule.run(&mut world);
assert_eq!(
world
.get::<ComputedNodeTarget>(uinode)
.unwrap()
.scale_factor,
scale2
);
assert_eq!(
world
.get::<ComputedNodeTarget>(uinode)
.unwrap()
.physical_size,
size2
);
assert_eq!(
world
.get::<ComputedNodeTarget>(uinode)
.unwrap()
.camera()
.unwrap(),
camera2
);
}
#[test]
fn update_context_after_parent_removed() {
let (mut world, mut schedule) = setup_test_world_and_schedule();
let scale1 = 1.;
let size1 = UVec2::new(100, 100);
let scale2 = 2.;
let size2 = UVec2::new(200, 200);
world.spawn((
Window {
resolution: WindowResolution::new(size1.x as f32, size1.y as f32)
.with_scale_factor_override(scale1),
..Default::default()
},
PrimaryWindow,
));
let window_2 = world
.spawn((Window {
resolution: WindowResolution::new(size2.x as f32, size2.y as f32)
.with_scale_factor_override(scale2),
..Default::default()
},))
.id();
let camera1 = world.spawn((Camera2d, IsDefaultUiCamera)).id();
let camera2 = world
.spawn((
Camera2d,
Camera {
target: RenderTarget::Window(WindowRef::Entity(window_2)),
..default()
},
))
.id();
// `UiTargetCamera` is ignored on non-root UI nodes
let uinode1 = world.spawn((Node::default(), UiTargetCamera(camera2))).id();
let uinode2 = world.spawn(Node::default()).add_child(uinode1).id();
schedule.run(&mut world);
assert_eq!(
world
.get::<ComputedNodeTarget>(uinode1)
.unwrap()
.scale_factor(),
scale1
);
assert_eq!(
world
.get::<ComputedNodeTarget>(uinode1)
.unwrap()
.physical_size(),
size1
);
assert_eq!(
world
.get::<ComputedNodeTarget>(uinode1)
.unwrap()
.camera()
.unwrap(),
camera1
);
assert_eq!(
world
.get::<ComputedNodeTarget>(uinode2)
.unwrap()
.camera()
.unwrap(),
camera1
);
// Now `uinode1` is a root UI node its `UiTargetCamera` component will be used and its camera target set to `camera2`.
world.entity_mut(uinode1).remove::<ChildOf>();
schedule.run(&mut world);
assert_eq!(
world
.get::<ComputedNodeTarget>(uinode1)
.unwrap()
.scale_factor(),
scale2
);
assert_eq!(
world
.get::<ComputedNodeTarget>(uinode1)
.unwrap()
.physical_size(),
size2
);
assert_eq!(
world
.get::<ComputedNodeTarget>(uinode1)
.unwrap()
.camera()
.unwrap(),
camera2
);
assert_eq!(
world
.get::<ComputedNodeTarget>(uinode2)
.unwrap()
.camera()
.unwrap(),
camera1
);
}
#[test]
fn update_great_grandchild() {
let (mut world, mut schedule) = setup_test_world_and_schedule();
let scale = 1.;
let size = UVec2::new(100, 100);
world.spawn((
Window {
resolution: WindowResolution::new(size.x as f32, size.y as f32)
.with_scale_factor_override(scale),
..Default::default()
},
PrimaryWindow,
));
let camera = world.spawn(Camera2d).id();
let uinode = world.spawn(Node::default()).id();
world.spawn(Node::default()).with_children(|builder| {
builder.spawn(Node::default()).with_children(|builder| {
builder.spawn(Node::default()).add_child(uinode);
});
});
schedule.run(&mut world);
assert_eq!(
world
.get::<ComputedNodeTarget>(uinode)
.unwrap()
.scale_factor,
scale
);
assert_eq!(
world
.get::<ComputedNodeTarget>(uinode)
.unwrap()
.physical_size,
size
);
assert_eq!(
world
.get::<ComputedNodeTarget>(uinode)
.unwrap()
.camera()
.unwrap(),
camera
);
world.resource_mut::<UiScale>().0 = 2.;
schedule.run(&mut world);
assert_eq!(
world
.get::<ComputedNodeTarget>(uinode)
.unwrap()
.scale_factor(),
2.
);
}
}

9
vendor/bevy_ui/src/widget/button.rs vendored Normal file
View File

@@ -0,0 +1,9 @@
use crate::{FocusPolicy, Interaction, Node};
use bevy_ecs::{component::Component, reflect::ReflectComponent};
use bevy_reflect::{std_traits::ReflectDefault, Reflect};
/// Marker struct for buttons
#[derive(Component, Debug, Default, Clone, Copy, PartialEq, Eq, Reflect)]
#[reflect(Component, Default, Debug, PartialEq, Clone)]
#[require(Node, FocusPolicy::Block, Interaction)]
pub struct Button;

299
vendor/bevy_ui/src/widget/image.rs vendored Normal file
View File

@@ -0,0 +1,299 @@
use crate::{ComputedNodeTarget, ContentSize, Measure, MeasureArgs, Node, NodeMeasure};
use bevy_asset::{Assets, Handle};
use bevy_color::Color;
use bevy_ecs::prelude::*;
use bevy_image::prelude::*;
use bevy_math::{Rect, UVec2, Vec2};
use bevy_reflect::{std_traits::ReflectDefault, Reflect};
use bevy_render::texture::TRANSPARENT_IMAGE_HANDLE;
use bevy_sprite::TextureSlicer;
use taffy::{MaybeMath, MaybeResolve};
/// A UI Node that renders an image.
#[derive(Component, Clone, Debug, Reflect)]
#[reflect(Component, Default, Debug, Clone)]
#[require(Node, ImageNodeSize, ContentSize)]
pub struct ImageNode {
/// The tint color used to draw the image.
///
/// This is multiplied by the color of each pixel in the image.
/// The field value defaults to solid white, which will pass the image through unmodified.
pub color: Color,
/// Handle to the texture.
///
/// This defaults to a [`TRANSPARENT_IMAGE_HANDLE`], which points to a fully transparent 1x1 texture.
pub image: Handle<Image>,
/// The (optional) texture atlas used to render the image.
pub texture_atlas: Option<TextureAtlas>,
/// Whether the image should be flipped along its x-axis.
pub flip_x: bool,
/// Whether the image should be flipped along its y-axis.
pub flip_y: bool,
/// An optional rectangle representing the region of the image to render, instead of rendering
/// the full image. This is an easy one-off alternative to using a [`TextureAtlas`].
///
/// When used with a [`TextureAtlas`], the rect
/// is offset by the atlas's minimal (top-left) corner position.
pub rect: Option<Rect>,
/// Controls how the image is altered to fit within the layout and how the layout algorithm determines the space to allocate for the image.
pub image_mode: NodeImageMode,
}
impl Default for ImageNode {
/// A transparent 1x1 image with a solid white tint.
///
/// # Warning
///
/// This will be invisible by default.
/// To set this to a visible image, you need to set the `texture` field to a valid image handle,
/// or use [`Handle<Image>`]'s default 1x1 solid white texture (as is done in [`ImageNode::solid_color`]).
fn default() -> Self {
ImageNode {
// This should be white because the tint is multiplied with the image,
// so if you set an actual image with default tint you'd want its original colors
color: Color::WHITE,
texture_atlas: None,
// This texture needs to be transparent by default, to avoid covering the background color
image: TRANSPARENT_IMAGE_HANDLE,
flip_x: false,
flip_y: false,
rect: None,
image_mode: NodeImageMode::Auto,
}
}
}
impl ImageNode {
/// Create a new [`ImageNode`] with the given texture.
pub fn new(texture: Handle<Image>) -> Self {
Self {
image: texture,
color: Color::WHITE,
..Default::default()
}
}
/// Create a solid color [`ImageNode`].
///
/// This is primarily useful for debugging / mocking the extents of your image.
pub fn solid_color(color: Color) -> Self {
Self {
image: Handle::default(),
color,
flip_x: false,
flip_y: false,
texture_atlas: None,
rect: None,
image_mode: NodeImageMode::Auto,
}
}
/// Create a [`ImageNode`] from an image, with an associated texture atlas
pub fn from_atlas_image(image: Handle<Image>, atlas: TextureAtlas) -> Self {
Self {
image,
texture_atlas: Some(atlas),
..Default::default()
}
}
/// Set the color tint
#[must_use]
pub const fn with_color(mut self, color: Color) -> Self {
self.color = color;
self
}
/// Flip the image along its x-axis
#[must_use]
pub const fn with_flip_x(mut self) -> Self {
self.flip_x = true;
self
}
/// Flip the image along its y-axis
#[must_use]
pub const fn with_flip_y(mut self) -> Self {
self.flip_y = true;
self
}
#[must_use]
pub const fn with_rect(mut self, rect: Rect) -> Self {
self.rect = Some(rect);
self
}
#[must_use]
pub const fn with_mode(mut self, mode: NodeImageMode) -> Self {
self.image_mode = mode;
self
}
}
impl From<Handle<Image>> for ImageNode {
fn from(texture: Handle<Image>) -> Self {
Self::new(texture)
}
}
/// Controls how the image is altered to fit within the layout and how the layout algorithm determines the space in the layout for the image
#[derive(Default, Debug, Clone, Reflect)]
#[reflect(Clone, Default)]
pub enum NodeImageMode {
/// The image will be sized automatically by taking the size of the source image and applying any layout constraints.
#[default]
Auto,
/// The image will be resized to match the size of the node. The image's original size and aspect ratio will be ignored.
Stretch,
/// The texture will be cut in 9 slices, keeping the texture in proportions on resize
Sliced(TextureSlicer),
/// The texture will be repeated if stretched beyond `stretched_value`
Tiled {
/// Should the image repeat horizontally
tile_x: bool,
/// Should the image repeat vertically
tile_y: bool,
/// The texture will repeat when the ratio between the *drawing dimensions* of texture and the
/// *original texture size* are above this value.
stretch_value: f32,
},
}
impl NodeImageMode {
/// Returns true if this mode uses slices internally ([`NodeImageMode::Sliced`] or [`NodeImageMode::Tiled`])
#[inline]
pub fn uses_slices(&self) -> bool {
matches!(
self,
NodeImageMode::Sliced(..) | NodeImageMode::Tiled { .. }
)
}
}
/// The size of the image's texture
///
/// This component is updated automatically by [`update_image_content_size_system`]
#[derive(Component, Debug, Copy, Clone, Default, Reflect)]
#[reflect(Component, Default, Debug, Clone)]
pub struct ImageNodeSize {
/// The size of the image's texture
///
/// This field is updated automatically by [`update_image_content_size_system`]
size: UVec2,
}
impl ImageNodeSize {
/// The size of the image's texture
pub fn size(&self) -> UVec2 {
self.size
}
}
#[derive(Clone)]
/// Used to calculate the size of UI image nodes
pub struct ImageMeasure {
/// The size of the image's texture
pub size: Vec2,
}
impl Measure for ImageMeasure {
fn measure(&mut self, measure_args: MeasureArgs, style: &taffy::Style) -> Vec2 {
let MeasureArgs {
width,
height,
available_width,
available_height,
..
} = measure_args;
// Convert available width/height into an option
let parent_width = available_width.into_option();
let parent_height = available_height.into_option();
// Resolve styles
let s_aspect_ratio = style.aspect_ratio;
let s_width = style.size.width.maybe_resolve(parent_width);
let s_min_width = style.min_size.width.maybe_resolve(parent_width);
let s_max_width = style.max_size.width.maybe_resolve(parent_width);
let s_height = style.size.height.maybe_resolve(parent_height);
let s_min_height = style.min_size.height.maybe_resolve(parent_height);
let s_max_height = style.max_size.height.maybe_resolve(parent_height);
// Determine width and height from styles and known_sizes (if a size is available
// from any of these sources)
let width = width.or(s_width
.or(s_min_width)
.maybe_clamp(s_min_width, s_max_width));
let height = height.or(s_height
.or(s_min_height)
.maybe_clamp(s_min_height, s_max_height));
// Use aspect_ratio from style, fall back to inherent aspect ratio
let aspect_ratio = s_aspect_ratio.unwrap_or_else(|| self.size.x / self.size.y);
// Apply aspect ratio
// If only one of width or height was determined at this point, then the other is set beyond this point using the aspect ratio.
let taffy_size = taffy::Size { width, height }.maybe_apply_aspect_ratio(Some(aspect_ratio));
// Use computed sizes or fall back to image's inherent size
Vec2 {
x: taffy_size
.width
.unwrap_or(self.size.x)
.maybe_clamp(s_min_width, s_max_width),
y: taffy_size
.height
.unwrap_or(self.size.y)
.maybe_clamp(s_min_height, s_max_height),
}
}
}
type UpdateImageFilter = (With<Node>, Without<crate::prelude::Text>);
/// Updates content size of the node based on the image provided
pub fn update_image_content_size_system(
textures: Res<Assets<Image>>,
atlases: Res<Assets<TextureAtlasLayout>>,
mut query: Query<
(
&mut ContentSize,
Ref<ImageNode>,
&mut ImageNodeSize,
Ref<ComputedNodeTarget>,
),
UpdateImageFilter,
>,
) {
for (mut content_size, image, mut image_size, computed_target) in &mut query {
if !matches!(image.image_mode, NodeImageMode::Auto)
|| image.image.id() == TRANSPARENT_IMAGE_HANDLE.id()
{
if image.is_changed() {
// Mutably derefs, marking the `ContentSize` as changed ensuring `ui_layout_system` will remove the node's measure func if present.
content_size.measure = None;
}
continue;
}
if let Some(size) =
image
.rect
.map(|rect| rect.size().as_uvec2())
.or_else(|| match &image.texture_atlas {
Some(atlas) => atlas.texture_rect(&atlases).map(|t| t.size()),
None => textures.get(&image.image).map(Image::size),
})
{
// Update only if size or scale factor has changed to avoid needless layout calculations
if size != image_size.size || computed_target.is_changed() || content_size.is_added() {
image_size.size = size;
content_size.set(NodeMeasure::Image(ImageMeasure {
// multiply the image size by the scale factor to get the physical size
size: size.as_vec2() * computed_target.scale_factor(),
}));
}
}
}
}

7
vendor/bevy_ui/src/widget/label.rs vendored Normal file
View File

@@ -0,0 +1,7 @@
use bevy_ecs::{prelude::Component, reflect::ReflectComponent};
use bevy_reflect::{std_traits::ReflectDefault, Reflect};
/// Marker struct for labels
#[derive(Component, Debug, Default, Clone, Copy, Reflect)]
#[reflect(Component, Default, Debug, Clone)]
pub struct Label;

13
vendor/bevy_ui/src/widget/mod.rs vendored Normal file
View File

@@ -0,0 +1,13 @@
//! This module contains the basic building blocks of Bevy's UI
mod button;
mod image;
mod label;
mod text;
pub use button::*;
pub use image::*;
pub use label::*;
pub use text::*;

399
vendor/bevy_ui/src/widget/text.rs vendored Normal file
View File

@@ -0,0 +1,399 @@
use crate::{
ComputedNode, ComputedNodeTarget, ContentSize, FixedMeasure, Measure, MeasureArgs, Node,
NodeMeasure,
};
use bevy_asset::Assets;
use bevy_color::Color;
use bevy_derive::{Deref, DerefMut};
use bevy_ecs::{
change_detection::DetectChanges,
component::Component,
entity::Entity,
query::With,
reflect::ReflectComponent,
system::{Query, Res, ResMut},
world::{Mut, Ref},
};
use bevy_image::prelude::*;
use bevy_math::Vec2;
use bevy_reflect::{std_traits::ReflectDefault, Reflect};
use bevy_text::{
scale_value, ComputedTextBlock, CosmicFontSystem, Font, FontAtlasSets, LineBreak, SwashCache,
TextBounds, TextColor, TextError, TextFont, TextLayout, TextLayoutInfo, TextMeasureInfo,
TextPipeline, TextReader, TextRoot, TextSpanAccess, TextWriter, YAxisOrientation,
};
use taffy::style::AvailableSpace;
use tracing::error;
/// UI text system flags.
///
/// Used internally by [`measure_text_system`] and [`text_system`] to schedule text for processing.
#[derive(Component, Debug, Clone, Reflect)]
#[reflect(Component, Default, Debug, Clone)]
pub struct TextNodeFlags {
/// If set then a new measure function for the text node will be created.
needs_measure_fn: bool,
/// If set then the text will be recomputed.
needs_recompute: bool,
}
impl Default for TextNodeFlags {
fn default() -> Self {
Self {
needs_measure_fn: true,
needs_recompute: true,
}
}
}
/// The top-level UI text component.
///
/// Adding [`Text`] to an entity will pull in required components for setting up a UI text node.
///
/// The string in this component is the first 'text span' in a hierarchy of text spans that are collected into
/// a [`ComputedTextBlock`]. See [`TextSpan`](bevy_text::TextSpan) for the component used by children of entities with [`Text`].
///
/// Note that [`Transform`](bevy_transform::components::Transform) on this entity is managed automatically by the UI layout system.
///
///
/// ```
/// # use bevy_asset::Handle;
/// # use bevy_color::Color;
/// # use bevy_color::palettes::basic::BLUE;
/// # use bevy_ecs::world::World;
/// # use bevy_text::{Font, JustifyText, TextLayout, TextFont, TextColor, TextSpan};
/// # use bevy_ui::prelude::Text;
/// #
/// # let font_handle: Handle<Font> = Default::default();
/// # let mut world = World::default();
/// #
/// // Basic usage.
/// world.spawn(Text::new("hello world!"));
///
/// // With non-default style.
/// world.spawn((
/// Text::new("hello world!"),
/// TextFont {
/// font: font_handle.clone().into(),
/// font_size: 60.0,
/// ..Default::default()
/// },
/// TextColor(BLUE.into()),
/// ));
///
/// // With text justification.
/// world.spawn((
/// Text::new("hello world\nand bevy!"),
/// TextLayout::new_with_justify(JustifyText::Center)
/// ));
///
/// // With spans
/// world.spawn(Text::new("hello ")).with_children(|parent| {
/// parent.spawn(TextSpan::new("world"));
/// parent.spawn((TextSpan::new("!"), TextColor(BLUE.into())));
/// });
/// ```
#[derive(Component, Debug, Default, Clone, Deref, DerefMut, Reflect, PartialEq)]
#[reflect(Component, Default, Debug, PartialEq, Clone)]
#[require(Node, TextLayout, TextFont, TextColor, TextNodeFlags, ContentSize)]
pub struct Text(pub String);
impl Text {
/// Makes a new text component.
pub fn new(text: impl Into<String>) -> Self {
Self(text.into())
}
}
impl TextRoot for Text {}
impl TextSpanAccess for Text {
fn read_span(&self) -> &str {
self.as_str()
}
fn write_span(&mut self) -> &mut String {
&mut *self
}
}
impl From<&str> for Text {
fn from(value: &str) -> Self {
Self(String::from(value))
}
}
impl From<String> for Text {
fn from(value: String) -> Self {
Self(value)
}
}
/// UI alias for [`TextReader`].
pub type TextUiReader<'w, 's> = TextReader<'w, 's, Text>;
/// UI alias for [`TextWriter`].
pub type TextUiWriter<'w, 's> = TextWriter<'w, 's, Text>;
/// Text measurement for UI layout. See [`NodeMeasure`].
pub struct TextMeasure {
pub info: TextMeasureInfo,
}
impl TextMeasure {
/// Checks if the cosmic text buffer is needed for measuring the text.
pub fn needs_buffer(height: Option<f32>, available_width: AvailableSpace) -> bool {
height.is_none() && matches!(available_width, AvailableSpace::Definite(_))
}
}
impl Measure for TextMeasure {
fn measure(&mut self, measure_args: MeasureArgs, _style: &taffy::Style) -> Vec2 {
let MeasureArgs {
width,
height,
available_width,
buffer,
font_system,
..
} = measure_args;
let x = width.unwrap_or_else(|| match available_width {
AvailableSpace::Definite(x) => {
// It is possible for the "min content width" to be larger than
// the "max content width" when soft-wrapping right-aligned text
// and possibly other situations.
x.max(self.info.min.x).min(self.info.max.x)
}
AvailableSpace::MinContent => self.info.min.x,
AvailableSpace::MaxContent => self.info.max.x,
});
height
.map_or_else(
|| match available_width {
AvailableSpace::Definite(_) => {
if let Some(buffer) = buffer {
self.info.compute_size(
TextBounds::new_horizontal(x),
buffer,
font_system,
)
} else {
error!("text measure failed, buffer is missing");
Vec2::default()
}
}
AvailableSpace::MinContent => Vec2::new(x, self.info.min.y),
AvailableSpace::MaxContent => Vec2::new(x, self.info.max.y),
},
|y| Vec2::new(x, y),
)
.ceil()
}
}
#[inline]
fn create_text_measure<'a>(
entity: Entity,
fonts: &Assets<Font>,
scale_factor: f64,
spans: impl Iterator<Item = (Entity, usize, &'a str, &'a TextFont, Color)>,
block: Ref<TextLayout>,
text_pipeline: &mut TextPipeline,
mut content_size: Mut<ContentSize>,
mut text_flags: Mut<TextNodeFlags>,
mut computed: Mut<ComputedTextBlock>,
font_system: &mut CosmicFontSystem,
) {
match text_pipeline.create_text_measure(
entity,
fonts,
spans,
scale_factor,
&block,
computed.as_mut(),
font_system,
) {
Ok(measure) => {
if block.linebreak == LineBreak::NoWrap {
content_size.set(NodeMeasure::Fixed(FixedMeasure { size: measure.max }));
} else {
content_size.set(NodeMeasure::Text(TextMeasure { info: measure }));
}
// Text measure func created successfully, so set `TextNodeFlags` to schedule a recompute
text_flags.needs_measure_fn = false;
text_flags.needs_recompute = true;
}
Err(TextError::NoSuchFont) => {
// Try again next frame
text_flags.needs_measure_fn = true;
}
Err(e @ (TextError::FailedToAddGlyph(_) | TextError::FailedToGetGlyphImage(_))) => {
panic!("Fatal error when processing text: {e}.");
}
};
}
/// Generates a new [`Measure`] for a text node on changes to its [`Text`] component.
///
/// A `Measure` is used by the UI's layout algorithm to determine the appropriate amount of space
/// to provide for the text given the fonts, the text itself and the constraints of the layout.
///
/// * Measures are regenerated on changes to either [`ComputedTextBlock`] or [`ComputedNodeTarget`].
/// * Changes that only modify the colors of a `Text` do not require a new `Measure`. This system
/// is only able to detect that a `Text` component has changed and will regenerate the `Measure` on
/// color changes. This can be expensive, particularly for large blocks of text, and the [`bypass_change_detection`](bevy_ecs::change_detection::DetectChangesMut::bypass_change_detection)
/// method should be called when only changing the `Text`'s colors.
pub fn measure_text_system(
fonts: Res<Assets<Font>>,
mut text_query: Query<
(
Entity,
Ref<TextLayout>,
&mut ContentSize,
&mut TextNodeFlags,
&mut ComputedTextBlock,
Ref<ComputedNodeTarget>,
),
With<Node>,
>,
mut text_reader: TextUiReader,
mut text_pipeline: ResMut<TextPipeline>,
mut font_system: ResMut<CosmicFontSystem>,
) {
for (entity, block, content_size, text_flags, computed, computed_target) in &mut text_query {
// Note: the ComputedTextBlock::needs_rerender bool is cleared in create_text_measure().
if computed_target.is_changed()
|| computed.needs_rerender()
|| text_flags.needs_measure_fn
|| content_size.is_added()
{
create_text_measure(
entity,
&fonts,
computed_target.scale_factor.into(),
text_reader.iter(entity),
block,
&mut text_pipeline,
content_size,
text_flags,
computed,
&mut font_system,
);
}
}
}
#[inline]
fn queue_text(
entity: Entity,
fonts: &Assets<Font>,
text_pipeline: &mut TextPipeline,
font_atlas_sets: &mut FontAtlasSets,
texture_atlases: &mut Assets<TextureAtlasLayout>,
textures: &mut Assets<Image>,
scale_factor: f32,
inverse_scale_factor: f32,
block: &TextLayout,
node: Ref<ComputedNode>,
mut text_flags: Mut<TextNodeFlags>,
text_layout_info: Mut<TextLayoutInfo>,
computed: &mut ComputedTextBlock,
text_reader: &mut TextUiReader,
font_system: &mut CosmicFontSystem,
swash_cache: &mut SwashCache,
) {
// Skip the text node if it is waiting for a new measure func
if text_flags.needs_measure_fn {
return;
}
let physical_node_size = if block.linebreak == LineBreak::NoWrap {
// With `NoWrap` set, no constraints are placed on the width of the text.
TextBounds::UNBOUNDED
} else {
// `scale_factor` is already multiplied by `UiScale`
TextBounds::new(node.unrounded_size.x, node.unrounded_size.y)
};
let text_layout_info = text_layout_info.into_inner();
match text_pipeline.queue_text(
text_layout_info,
fonts,
text_reader.iter(entity),
scale_factor.into(),
block,
physical_node_size,
font_atlas_sets,
texture_atlases,
textures,
YAxisOrientation::TopToBottom,
computed,
font_system,
swash_cache,
) {
Err(TextError::NoSuchFont) => {
// There was an error processing the text layout, try again next frame
text_flags.needs_recompute = true;
}
Err(e @ (TextError::FailedToAddGlyph(_) | TextError::FailedToGetGlyphImage(_))) => {
panic!("Fatal error when processing text: {e}.");
}
Ok(()) => {
text_layout_info.size.x = scale_value(text_layout_info.size.x, inverse_scale_factor);
text_layout_info.size.y = scale_value(text_layout_info.size.y, inverse_scale_factor);
text_flags.needs_recompute = false;
}
}
}
/// Updates the layout and size information for a UI text node on changes to the size value of its [`Node`] component,
/// or when the `needs_recompute` field of [`TextNodeFlags`] is set to true.
/// This information is computed by the [`TextPipeline`] and then stored in [`TextLayoutInfo`].
///
/// ## World Resources
///
/// [`ResMut<Assets<Image>>`](Assets<Image>) -- This system only adds new [`Image`] assets.
/// It does not modify or observe existing ones. The exception is when adding new glyphs to a [`bevy_text::FontAtlas`].
pub fn text_system(
mut textures: ResMut<Assets<Image>>,
fonts: Res<Assets<Font>>,
mut texture_atlases: ResMut<Assets<TextureAtlasLayout>>,
mut font_atlas_sets: ResMut<FontAtlasSets>,
mut text_pipeline: ResMut<TextPipeline>,
mut text_query: Query<(
Entity,
Ref<ComputedNode>,
&TextLayout,
&mut TextLayoutInfo,
&mut TextNodeFlags,
&mut ComputedTextBlock,
)>,
mut text_reader: TextUiReader,
mut font_system: ResMut<CosmicFontSystem>,
mut swash_cache: ResMut<SwashCache>,
) {
for (entity, node, block, text_layout_info, text_flags, mut computed) in &mut text_query {
if node.is_changed() || text_flags.needs_recompute {
queue_text(
entity,
&fonts,
&mut text_pipeline,
&mut font_atlas_sets,
&mut texture_atlases,
&mut textures,
node.inverse_scale_factor.recip(),
node.inverse_scale_factor,
block,
node,
text_flags,
text_layout_info,
computed.as_mut(),
&mut text_reader,
&mut font_system,
&mut swash_cache,
);
}
}
}