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

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,
);
}
}
}