Ui Node Borders (#7795)
# Objective Implement borders for UI nodes. Relevant discussion: #7785 Related: #5924, #3991 <img width="283" alt="borders" src="https://user-images.githubusercontent.com/27962798/220968899-7661d5ec-6f5b-4b0f-af29-bf9af02259b5.PNG"> ## Solution Add an extraction function to draw the borders. --- Can only do one colour rectangular borders due to the limitations of the Bevy UI renderer. Maybe it can be combined with #3991 eventually to add curved border support. ## Changelog * Added a component `BorderColor`. * Added the `extract_uinode_borders` system to the UI Render App. * Added the UI example `borders` --------- Co-authored-by: Nico Burns <nico@nicoburns.com>
This commit is contained in:
		
							parent
							
								
									2551ccbe34
								
							
						
					
					
						commit
						f7aa83a247
					
				
							
								
								
									
										20
									
								
								Cargo.toml
									
									
									
									
									
								
							
							
						
						
									
										20
									
								
								Cargo.toml
									
									
									
									
									
								
							@ -1762,6 +1762,16 @@ category = "Transforms"
 | 
			
		||||
wasm = true
 | 
			
		||||
 | 
			
		||||
# UI (User Interface)
 | 
			
		||||
[[example]]
 | 
			
		||||
name = "borders"
 | 
			
		||||
path = "examples/ui/borders.rs"
 | 
			
		||||
 | 
			
		||||
[package.metadata.example.borders]
 | 
			
		||||
name = "Borders"
 | 
			
		||||
description = "Demonstrates how to create a node with a border"
 | 
			
		||||
category = "UI (User Interface)"
 | 
			
		||||
wasm = true
 | 
			
		||||
 | 
			
		||||
[[example]]
 | 
			
		||||
name = "button"
 | 
			
		||||
path = "examples/ui/button.rs"
 | 
			
		||||
@ -1923,6 +1933,16 @@ description = "Illustrates how to scale the UI"
 | 
			
		||||
category = "UI (User Interface)"
 | 
			
		||||
wasm = true
 | 
			
		||||
 | 
			
		||||
[[example]]
 | 
			
		||||
name = "viewport_debug"
 | 
			
		||||
path = "examples/ui/viewport_debug.rs"
 | 
			
		||||
 | 
			
		||||
[package.metadata.example.viewport_debug]
 | 
			
		||||
name = "Viewport Debug"
 | 
			
		||||
description = "An example for debugging viewport coordinates"
 | 
			
		||||
category = "UI (User Interface)"
 | 
			
		||||
wasm = true
 | 
			
		||||
 | 
			
		||||
# Window
 | 
			
		||||
[[example]]
 | 
			
		||||
name = "clear_color"
 | 
			
		||||
 | 
			
		||||
@ -116,6 +116,7 @@ impl Plugin for UiPlugin {
 | 
			
		||||
            .register_type::<UiImageSize>()
 | 
			
		||||
            .register_type::<UiRect>()
 | 
			
		||||
            .register_type::<Val>()
 | 
			
		||||
            .register_type::<BorderColor>()
 | 
			
		||||
            .register_type::<widget::Button>()
 | 
			
		||||
            .register_type::<widget::Label>()
 | 
			
		||||
            .register_type::<ZIndex>()
 | 
			
		||||
 | 
			
		||||
@ -2,7 +2,8 @@
 | 
			
		||||
 | 
			
		||||
use crate::{
 | 
			
		||||
    widget::{Button, TextFlags, UiImageSize},
 | 
			
		||||
    BackgroundColor, ContentSize, FocusPolicy, Interaction, Node, Style, UiImage, ZIndex,
 | 
			
		||||
    BackgroundColor, BorderColor, ContentSize, FocusPolicy, Interaction, Node, Style, UiImage,
 | 
			
		||||
    ZIndex,
 | 
			
		||||
};
 | 
			
		||||
use bevy_ecs::bundle::Bundle;
 | 
			
		||||
use bevy_render::{
 | 
			
		||||
@ -25,6 +26,8 @@ pub struct NodeBundle {
 | 
			
		||||
    pub style: Style,
 | 
			
		||||
    /// The background color, which serves as a "fill" for this node
 | 
			
		||||
    pub background_color: BackgroundColor,
 | 
			
		||||
    /// The color of the Node's border
 | 
			
		||||
    pub border_color: BorderColor,
 | 
			
		||||
    /// Whether this node should block interaction with lower nodes
 | 
			
		||||
    pub focus_policy: FocusPolicy,
 | 
			
		||||
    /// The transform of the node
 | 
			
		||||
@ -50,6 +53,7 @@ impl Default for NodeBundle {
 | 
			
		||||
        NodeBundle {
 | 
			
		||||
            // Transparent background
 | 
			
		||||
            background_color: Color::NONE.into(),
 | 
			
		||||
            border_color: Color::NONE.into(),
 | 
			
		||||
            node: Default::default(),
 | 
			
		||||
            style: Default::default(),
 | 
			
		||||
            focus_policy: Default::default(),
 | 
			
		||||
@ -225,6 +229,8 @@ pub struct ButtonBundle {
 | 
			
		||||
    ///
 | 
			
		||||
    /// When combined with `UiImage`, tints the provided image.
 | 
			
		||||
    pub background_color: BackgroundColor,
 | 
			
		||||
    /// The color of the Node's border
 | 
			
		||||
    pub border_color: BorderColor,
 | 
			
		||||
    /// The image of the node
 | 
			
		||||
    pub image: UiImage,
 | 
			
		||||
    /// The transform of the node
 | 
			
		||||
@ -252,6 +258,7 @@ impl Default for ButtonBundle {
 | 
			
		||||
            node: Default::default(),
 | 
			
		||||
            button: Default::default(),
 | 
			
		||||
            style: Default::default(),
 | 
			
		||||
            border_color: BorderColor(Color::NONE),
 | 
			
		||||
            interaction: Default::default(),
 | 
			
		||||
            background_color: Default::default(),
 | 
			
		||||
            image: Default::default(),
 | 
			
		||||
 | 
			
		||||
@ -2,13 +2,17 @@ mod pipeline;
 | 
			
		||||
mod render_pass;
 | 
			
		||||
 | 
			
		||||
use bevy_core_pipeline::{core_2d::Camera2d, core_3d::Camera3d};
 | 
			
		||||
use bevy_hierarchy::Parent;
 | 
			
		||||
use bevy_render::{ExtractSchedule, Render};
 | 
			
		||||
#[cfg(feature = "bevy_text")]
 | 
			
		||||
use bevy_window::{PrimaryWindow, Window};
 | 
			
		||||
pub use pipeline::*;
 | 
			
		||||
pub use render_pass::*;
 | 
			
		||||
 | 
			
		||||
use crate::{prelude::UiCameraConfig, BackgroundColor, CalculatedClip, Node, UiImage, UiStack};
 | 
			
		||||
use crate::{
 | 
			
		||||
    prelude::UiCameraConfig, BackgroundColor, BorderColor, CalculatedClip, Node, UiImage, UiStack,
 | 
			
		||||
};
 | 
			
		||||
use crate::{ContentSize, Style, Val};
 | 
			
		||||
use bevy_app::prelude::*;
 | 
			
		||||
use bevy_asset::{load_internal_asset, AssetEvent, Assets, Handle, HandleUntyped};
 | 
			
		||||
use bevy_ecs::prelude::*;
 | 
			
		||||
@ -78,6 +82,7 @@ pub fn build_ui_render(app: &mut App) {
 | 
			
		||||
                extract_default_ui_camera_view::<Camera2d>,
 | 
			
		||||
                extract_default_ui_camera_view::<Camera3d>,
 | 
			
		||||
                extract_uinodes.in_set(RenderUiSystem::ExtractNode),
 | 
			
		||||
                extract_uinode_borders.after(RenderUiSystem::ExtractNode),
 | 
			
		||||
                #[cfg(feature = "bevy_text")]
 | 
			
		||||
                extract_text_uinodes.after(RenderUiSystem::ExtractNode),
 | 
			
		||||
            ),
 | 
			
		||||
@ -161,6 +166,123 @@ pub struct ExtractedUiNodes {
 | 
			
		||||
    pub uinodes: Vec<ExtractedUiNode>,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
fn resolve_border_thickness(value: Val, parent_width: f32, viewport_size: Vec2) -> f32 {
 | 
			
		||||
    match value {
 | 
			
		||||
        Val::Auto => 0.,
 | 
			
		||||
        Val::Px(px) => px.max(0.),
 | 
			
		||||
        Val::Percent(percent) => (parent_width * percent / 100.).max(0.),
 | 
			
		||||
        Val::Vw(percent) => (viewport_size.x * percent / 100.).max(0.),
 | 
			
		||||
        Val::Vh(percent) => (viewport_size.y * percent / 100.).max(0.),
 | 
			
		||||
        Val::VMin(percent) => (viewport_size.min_element() * percent / 100.).max(0.),
 | 
			
		||||
        Val::VMax(percent) => (viewport_size.max_element() * percent / 100.).max(0.),
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
pub fn extract_uinode_borders(
 | 
			
		||||
    mut extracted_uinodes: ResMut<ExtractedUiNodes>,
 | 
			
		||||
    windows: Extract<Query<&Window, With<PrimaryWindow>>>,
 | 
			
		||||
    ui_stack: Extract<Res<UiStack>>,
 | 
			
		||||
    uinode_query: Extract<
 | 
			
		||||
        Query<
 | 
			
		||||
            (
 | 
			
		||||
                &Node,
 | 
			
		||||
                &GlobalTransform,
 | 
			
		||||
                &Style,
 | 
			
		||||
                &BorderColor,
 | 
			
		||||
                Option<&Parent>,
 | 
			
		||||
                &ComputedVisibility,
 | 
			
		||||
                Option<&CalculatedClip>,
 | 
			
		||||
            ),
 | 
			
		||||
            Without<ContentSize>,
 | 
			
		||||
        >,
 | 
			
		||||
    >,
 | 
			
		||||
    parent_node_query: Extract<Query<&Node, With<Parent>>>,
 | 
			
		||||
) {
 | 
			
		||||
    let image = bevy_render::texture::DEFAULT_IMAGE_HANDLE.typed();
 | 
			
		||||
 | 
			
		||||
    let viewport_size = windows
 | 
			
		||||
        .get_single()
 | 
			
		||||
        .map(|window| Vec2::new(window.resolution.width(), window.resolution.height()))
 | 
			
		||||
        .unwrap_or(Vec2::ZERO);
 | 
			
		||||
 | 
			
		||||
    for (stack_index, entity) in ui_stack.uinodes.iter().enumerate() {
 | 
			
		||||
        if let Ok((node, global_transform, style, border_color, parent, visibility, clip)) =
 | 
			
		||||
            uinode_query.get(*entity)
 | 
			
		||||
        {
 | 
			
		||||
            // Skip invisible borders
 | 
			
		||||
            if !visibility.is_visible()
 | 
			
		||||
                || border_color.0.a() == 0.0
 | 
			
		||||
                || node.size().x <= 0.
 | 
			
		||||
                || node.size().y <= 0.
 | 
			
		||||
            {
 | 
			
		||||
                continue;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            // Both vertical and horizontal percentage border values are calculated based on the width of the parent node
 | 
			
		||||
            // <https://developer.mozilla.org/en-US/docs/Web/CSS/border-width>
 | 
			
		||||
            let parent_width = parent
 | 
			
		||||
                .and_then(|parent| parent_node_query.get(parent.get()).ok())
 | 
			
		||||
                .map(|parent_node| parent_node.size().x)
 | 
			
		||||
                .unwrap_or(viewport_size.x);
 | 
			
		||||
            let left = resolve_border_thickness(style.border.left, parent_width, viewport_size);
 | 
			
		||||
            let right = resolve_border_thickness(style.border.right, parent_width, viewport_size);
 | 
			
		||||
            let top = resolve_border_thickness(style.border.top, parent_width, viewport_size);
 | 
			
		||||
            let bottom = resolve_border_thickness(style.border.bottom, parent_width, viewport_size);
 | 
			
		||||
 | 
			
		||||
            // Calculate the border rects, ensuring no overlap.
 | 
			
		||||
            // The border occupies the space between the node's bounding rect and the node's bounding rect inset in each direction by the node's corresponding border value.
 | 
			
		||||
            let max = 0.5 * node.size();
 | 
			
		||||
            let min = -max;
 | 
			
		||||
            let inner_min = min + Vec2::new(left, top);
 | 
			
		||||
            let inner_max = (max - Vec2::new(right, bottom)).max(inner_min);
 | 
			
		||||
            let border_rects = [
 | 
			
		||||
                // Left border
 | 
			
		||||
                Rect {
 | 
			
		||||
                    min,
 | 
			
		||||
                    max: Vec2::new(inner_min.x, max.y),
 | 
			
		||||
                },
 | 
			
		||||
                // Right border
 | 
			
		||||
                Rect {
 | 
			
		||||
                    min: Vec2::new(inner_max.x, min.y),
 | 
			
		||||
                    max,
 | 
			
		||||
                },
 | 
			
		||||
                // Top border
 | 
			
		||||
                Rect {
 | 
			
		||||
                    min: Vec2::new(inner_min.x, min.y),
 | 
			
		||||
                    max: Vec2::new(inner_max.x, inner_min.y),
 | 
			
		||||
                },
 | 
			
		||||
                // Bottom border
 | 
			
		||||
                Rect {
 | 
			
		||||
                    min: Vec2::new(inner_min.x, inner_max.y),
 | 
			
		||||
                    max: Vec2::new(inner_max.x, max.y),
 | 
			
		||||
                },
 | 
			
		||||
            ];
 | 
			
		||||
 | 
			
		||||
            let transform = global_transform.compute_matrix();
 | 
			
		||||
 | 
			
		||||
            for edge in border_rects {
 | 
			
		||||
                if edge.min.x < edge.max.x && edge.min.y < edge.max.y {
 | 
			
		||||
                    extracted_uinodes.uinodes.push(ExtractedUiNode {
 | 
			
		||||
                        stack_index,
 | 
			
		||||
                        // This translates the uinode's transform to the center of the current border rectangle
 | 
			
		||||
                        transform: transform * Mat4::from_translation(edge.center().extend(0.)),
 | 
			
		||||
                        color: border_color.0,
 | 
			
		||||
                        rect: Rect {
 | 
			
		||||
                            max: edge.size(),
 | 
			
		||||
                            ..Default::default()
 | 
			
		||||
                        },
 | 
			
		||||
                        image: image.clone_weak(),
 | 
			
		||||
                        atlas_size: None,
 | 
			
		||||
                        clip: clip.map(|clip| clip.clip),
 | 
			
		||||
                        flip_x: false,
 | 
			
		||||
                        flip_y: false,
 | 
			
		||||
                    });
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
pub fn extract_uinodes(
 | 
			
		||||
    mut extracted_uinodes: ResMut<ExtractedUiNodes>,
 | 
			
		||||
    images: Extract<Res<Assets<Image>>>,
 | 
			
		||||
@ -177,6 +299,7 @@ pub fn extract_uinodes(
 | 
			
		||||
    >,
 | 
			
		||||
) {
 | 
			
		||||
    extracted_uinodes.uinodes.clear();
 | 
			
		||||
 | 
			
		||||
    for (stack_index, entity) in ui_stack.uinodes.iter().enumerate() {
 | 
			
		||||
        if let Ok((uinode, transform, color, maybe_image, visibility, clip)) =
 | 
			
		||||
            uinode_query.get(*entity)
 | 
			
		||||
 | 
			
		||||
@ -1563,6 +1563,27 @@ impl From<Color> for BackgroundColor {
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/// The border color of the UI node.
 | 
			
		||||
#[derive(Component, Copy, Clone, Debug, Reflect, FromReflect)]
 | 
			
		||||
#[reflect(FromReflect, Component, Default)]
 | 
			
		||||
pub struct BorderColor(pub Color);
 | 
			
		||||
 | 
			
		||||
impl From<Color> for BorderColor {
 | 
			
		||||
    fn from(color: Color) -> Self {
 | 
			
		||||
        Self(color)
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
impl BorderColor {
 | 
			
		||||
    pub const DEFAULT: Self = BorderColor(Color::WHITE);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
impl Default for BorderColor {
 | 
			
		||||
    fn default() -> Self {
 | 
			
		||||
        Self::DEFAULT
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/// The 2D texture displayed for this UI node
 | 
			
		||||
#[derive(Component, Clone, Debug, Reflect)]
 | 
			
		||||
#[reflect(Component, Default)]
 | 
			
		||||
 | 
			
		||||
@ -335,6 +335,7 @@ Example | Description
 | 
			
		||||
 | 
			
		||||
Example | Description
 | 
			
		||||
--- | ---
 | 
			
		||||
[Borders](../examples/ui/borders.rs) | Demonstrates how to create a node with a border
 | 
			
		||||
[Button](../examples/ui/button.rs) | Illustrates creating and updating a button
 | 
			
		||||
[CSS Grid](../examples/ui/grid.rs) | An example for CSS Grid layout
 | 
			
		||||
[Flex Layout](../examples/ui/flex_layout.rs) | Demonstrates how the AlignItems and JustifyContent properties can be composed to layout nodes and position text
 | 
			
		||||
@ -350,6 +351,7 @@ Example | Description
 | 
			
		||||
[UI](../examples/ui/ui.rs) | Illustrates various features of Bevy UI
 | 
			
		||||
[UI Scaling](../examples/ui/ui_scaling.rs) | Illustrates how to scale the UI
 | 
			
		||||
[UI Z-Index](../examples/ui/z_index.rs) | Demonstrates how to control the relative depth (z-position) of UI elements
 | 
			
		||||
[Viewport Debug](../examples/ui/viewport_debug.rs) | An example for debugging viewport coordinates
 | 
			
		||||
[Window Fallthrough](../examples/ui/window_fallthrough.rs) | Illustrates how to access `winit::window::Window`'s `hittest` functionality.
 | 
			
		||||
 | 
			
		||||
## Window
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										117
									
								
								examples/ui/borders.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										117
									
								
								examples/ui/borders.rs
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,117 @@
 | 
			
		||||
//! Example demonstrating bordered UI nodes
 | 
			
		||||
 | 
			
		||||
use bevy::prelude::*;
 | 
			
		||||
 | 
			
		||||
fn main() {
 | 
			
		||||
    App::new()
 | 
			
		||||
        .add_plugins(DefaultPlugins)
 | 
			
		||||
        .add_systems(Startup, setup)
 | 
			
		||||
        .run();
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
fn setup(mut commands: Commands) {
 | 
			
		||||
    commands.spawn(Camera2dBundle::default());
 | 
			
		||||
    let root = commands
 | 
			
		||||
        .spawn(NodeBundle {
 | 
			
		||||
            style: Style {
 | 
			
		||||
                flex_basis: Val::Percent(100.0),
 | 
			
		||||
                margin: UiRect::all(Val::Px(25.0)),
 | 
			
		||||
                flex_wrap: FlexWrap::Wrap,
 | 
			
		||||
                justify_content: JustifyContent::FlexStart,
 | 
			
		||||
                align_items: AlignItems::FlexStart,
 | 
			
		||||
                align_content: AlignContent::FlexStart,
 | 
			
		||||
                ..Default::default()
 | 
			
		||||
            },
 | 
			
		||||
            background_color: BackgroundColor(Color::BLACK),
 | 
			
		||||
            ..Default::default()
 | 
			
		||||
        })
 | 
			
		||||
        .id();
 | 
			
		||||
 | 
			
		||||
    // all the different combinations of border edges
 | 
			
		||||
    let borders = [
 | 
			
		||||
        UiRect::default(),
 | 
			
		||||
        UiRect::all(Val::Px(10.)),
 | 
			
		||||
        UiRect::left(Val::Px(10.)),
 | 
			
		||||
        UiRect::right(Val::Px(10.)),
 | 
			
		||||
        UiRect::top(Val::Px(10.)),
 | 
			
		||||
        UiRect::bottom(Val::Px(10.)),
 | 
			
		||||
        UiRect::horizontal(Val::Px(10.)),
 | 
			
		||||
        UiRect::vertical(Val::Px(10.)),
 | 
			
		||||
        UiRect {
 | 
			
		||||
            left: Val::Px(10.),
 | 
			
		||||
            top: Val::Px(10.),
 | 
			
		||||
            ..Default::default()
 | 
			
		||||
        },
 | 
			
		||||
        UiRect {
 | 
			
		||||
            left: Val::Px(10.),
 | 
			
		||||
            bottom: Val::Px(10.),
 | 
			
		||||
            ..Default::default()
 | 
			
		||||
        },
 | 
			
		||||
        UiRect {
 | 
			
		||||
            right: Val::Px(10.),
 | 
			
		||||
            top: Val::Px(10.),
 | 
			
		||||
            ..Default::default()
 | 
			
		||||
        },
 | 
			
		||||
        UiRect {
 | 
			
		||||
            right: Val::Px(10.),
 | 
			
		||||
            bottom: Val::Px(10.),
 | 
			
		||||
            ..Default::default()
 | 
			
		||||
        },
 | 
			
		||||
        UiRect {
 | 
			
		||||
            right: Val::Px(10.),
 | 
			
		||||
            top: Val::Px(10.),
 | 
			
		||||
            bottom: Val::Px(10.),
 | 
			
		||||
            ..Default::default()
 | 
			
		||||
        },
 | 
			
		||||
        UiRect {
 | 
			
		||||
            left: Val::Px(10.),
 | 
			
		||||
            top: Val::Px(10.),
 | 
			
		||||
            bottom: Val::Px(10.),
 | 
			
		||||
            ..Default::default()
 | 
			
		||||
        },
 | 
			
		||||
        UiRect {
 | 
			
		||||
            left: Val::Px(10.),
 | 
			
		||||
            right: Val::Px(10.),
 | 
			
		||||
            top: Val::Px(10.),
 | 
			
		||||
            ..Default::default()
 | 
			
		||||
        },
 | 
			
		||||
        UiRect {
 | 
			
		||||
            left: Val::Px(10.),
 | 
			
		||||
            right: Val::Px(10.),
 | 
			
		||||
            bottom: Val::Px(10.),
 | 
			
		||||
            ..Default::default()
 | 
			
		||||
        },
 | 
			
		||||
    ];
 | 
			
		||||
 | 
			
		||||
    for i in 0..64 {
 | 
			
		||||
        let inner_spot = commands
 | 
			
		||||
            .spawn(NodeBundle {
 | 
			
		||||
                style: Style {
 | 
			
		||||
                    width: Val::Px(10.),
 | 
			
		||||
                    height: Val::Px(10.),
 | 
			
		||||
                    ..Default::default()
 | 
			
		||||
                },
 | 
			
		||||
                background_color: Color::YELLOW.into(),
 | 
			
		||||
                ..Default::default()
 | 
			
		||||
            })
 | 
			
		||||
            .id();
 | 
			
		||||
        let bordered_node = commands
 | 
			
		||||
            .spawn(NodeBundle {
 | 
			
		||||
                style: Style {
 | 
			
		||||
                    width: Val::Px(50.),
 | 
			
		||||
                    height: Val::Px(50.),
 | 
			
		||||
                    border: borders[i % borders.len()],
 | 
			
		||||
                    margin: UiRect::all(Val::Px(2.)),
 | 
			
		||||
                    align_items: AlignItems::Center,
 | 
			
		||||
                    justify_content: JustifyContent::Center,
 | 
			
		||||
                    ..Default::default()
 | 
			
		||||
                },
 | 
			
		||||
                background_color: Color::BLUE.into(),
 | 
			
		||||
                border_color: Color::WHITE.with_a(0.5).into(),
 | 
			
		||||
                ..Default::default()
 | 
			
		||||
            })
 | 
			
		||||
            .add_child(inner_spot)
 | 
			
		||||
            .id();
 | 
			
		||||
        commands.entity(root).add_child(bordered_node);
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@ -19,25 +19,33 @@ const PRESSED_BUTTON: Color = Color::rgb(0.35, 0.75, 0.35);
 | 
			
		||||
 | 
			
		||||
fn button_system(
 | 
			
		||||
    mut interaction_query: Query<
 | 
			
		||||
        (&Interaction, &mut BackgroundColor, &Children),
 | 
			
		||||
        (
 | 
			
		||||
            &Interaction,
 | 
			
		||||
            &mut BackgroundColor,
 | 
			
		||||
            &mut BorderColor,
 | 
			
		||||
            &Children,
 | 
			
		||||
        ),
 | 
			
		||||
        (Changed<Interaction>, With<Button>),
 | 
			
		||||
    >,
 | 
			
		||||
    mut text_query: Query<&mut Text>,
 | 
			
		||||
) {
 | 
			
		||||
    for (interaction, mut color, children) in &mut interaction_query {
 | 
			
		||||
    for (interaction, mut color, mut border_color, children) in &mut interaction_query {
 | 
			
		||||
        let mut text = text_query.get_mut(children[0]).unwrap();
 | 
			
		||||
        match *interaction {
 | 
			
		||||
            Interaction::Clicked => {
 | 
			
		||||
                text.sections[0].value = "Press".to_string();
 | 
			
		||||
                *color = PRESSED_BUTTON.into();
 | 
			
		||||
                border_color.0 = Color::RED;
 | 
			
		||||
            }
 | 
			
		||||
            Interaction::Hovered => {
 | 
			
		||||
                text.sections[0].value = "Hover".to_string();
 | 
			
		||||
                *color = HOVERED_BUTTON.into();
 | 
			
		||||
                border_color.0 = Color::WHITE;
 | 
			
		||||
            }
 | 
			
		||||
            Interaction::None => {
 | 
			
		||||
                text.sections[0].value = "Button".to_string();
 | 
			
		||||
                *color = NORMAL_BUTTON.into();
 | 
			
		||||
                border_color.0 = Color::BLACK;
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
@ -62,12 +70,14 @@ fn setup(mut commands: Commands, asset_server: Res<AssetServer>) {
 | 
			
		||||
                    style: Style {
 | 
			
		||||
                        width: Val::Px(150.0),
 | 
			
		||||
                        height: Val::Px(65.0),
 | 
			
		||||
                        border: UiRect::all(Val::Px(5.0)),
 | 
			
		||||
                        // horizontally center child text
 | 
			
		||||
                        justify_content: JustifyContent::Center,
 | 
			
		||||
                        // vertically center child text
 | 
			
		||||
                        align_items: AlignItems::Center,
 | 
			
		||||
                        ..default()
 | 
			
		||||
                    },
 | 
			
		||||
                    border_color: BorderColor(Color::BLACK),
 | 
			
		||||
                    background_color: NORMAL_BUTTON.into(),
 | 
			
		||||
                    ..default()
 | 
			
		||||
                })
 | 
			
		||||
 | 
			
		||||
@ -165,6 +165,7 @@ fn setup(mut commands: Commands, asset_server: Res<AssetServer>) {
 | 
			
		||||
                        border: UiRect::all(Val::Px(20.)),
 | 
			
		||||
                        ..default()
 | 
			
		||||
                    },
 | 
			
		||||
                    border_color: Color::GREEN.into(),
 | 
			
		||||
                    background_color: Color::rgb(0.4, 0.4, 1.).into(),
 | 
			
		||||
                    ..default()
 | 
			
		||||
                })
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										241
									
								
								examples/ui/viewport_debug.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										241
									
								
								examples/ui/viewport_debug.rs
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,241 @@
 | 
			
		||||
//! An example for debugging viewport coordinates
 | 
			
		||||
 | 
			
		||||
use bevy::prelude::*;
 | 
			
		||||
 | 
			
		||||
const PALETTE: [Color; 10] = [
 | 
			
		||||
    Color::ORANGE,
 | 
			
		||||
    Color::BLUE,
 | 
			
		||||
    Color::WHITE,
 | 
			
		||||
    Color::BEIGE,
 | 
			
		||||
    Color::CYAN,
 | 
			
		||||
    Color::CRIMSON,
 | 
			
		||||
    Color::NAVY,
 | 
			
		||||
    Color::AZURE,
 | 
			
		||||
    Color::GREEN,
 | 
			
		||||
    Color::BLACK,
 | 
			
		||||
];
 | 
			
		||||
 | 
			
		||||
#[derive(Default, Debug, Hash, Eq, PartialEq, Clone, States)]
 | 
			
		||||
enum Coords {
 | 
			
		||||
    #[default]
 | 
			
		||||
    Viewport,
 | 
			
		||||
    Pixel,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
fn main() {
 | 
			
		||||
    App::new()
 | 
			
		||||
        .add_plugins(DefaultPlugins.set(WindowPlugin {
 | 
			
		||||
            primary_window: Some(Window {
 | 
			
		||||
                resolution: [800., 600.].into(),
 | 
			
		||||
                title: "Viewport Coordinates Debug".to_string(),
 | 
			
		||||
                resizable: false,
 | 
			
		||||
                ..Default::default()
 | 
			
		||||
            }),
 | 
			
		||||
            ..Default::default()
 | 
			
		||||
        }))
 | 
			
		||||
        .add_state::<Coords>()
 | 
			
		||||
        .add_systems(Startup, setup)
 | 
			
		||||
        .add_systems(OnEnter(Coords::Viewport), spawn_with_viewport_coords)
 | 
			
		||||
        .add_systems(OnEnter(Coords::Pixel), spawn_with_pixel_coords)
 | 
			
		||||
        .add_systems(OnExit(Coords::Viewport), despawn_nodes)
 | 
			
		||||
        .add_systems(OnExit(Coords::Pixel), despawn_nodes)
 | 
			
		||||
        .add_systems(Update, update)
 | 
			
		||||
        .run();
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
fn despawn_nodes(mut commands: Commands, query: Query<Entity, With<Node>>) {
 | 
			
		||||
    for entity in query.iter() {
 | 
			
		||||
        commands.entity(entity).despawn();
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
fn update(
 | 
			
		||||
    mut timer: Local<f32>,
 | 
			
		||||
    time: Res<Time>,
 | 
			
		||||
    state: Res<State<Coords>>,
 | 
			
		||||
    mut next_state: ResMut<NextState<Coords>>,
 | 
			
		||||
) {
 | 
			
		||||
    *timer += time.delta_seconds();
 | 
			
		||||
    if 1. <= *timer {
 | 
			
		||||
        *timer = 0.;
 | 
			
		||||
        next_state.set(if *state.get() == Coords::Viewport {
 | 
			
		||||
            Coords::Pixel
 | 
			
		||||
        } else {
 | 
			
		||||
            Coords::Viewport
 | 
			
		||||
        });
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
fn setup(mut commands: Commands) {
 | 
			
		||||
    commands.spawn(Camera2dBundle::default());
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
fn spawn_with_viewport_coords(mut commands: Commands) {
 | 
			
		||||
    commands
 | 
			
		||||
        .spawn(NodeBundle {
 | 
			
		||||
            style: Style {
 | 
			
		||||
                width: Val::Vw(100.),
 | 
			
		||||
                height: Val::Vh(100.),
 | 
			
		||||
                border: UiRect::axes(Val::Vw(5.), Val::Vh(5.)),
 | 
			
		||||
                flex_wrap: FlexWrap::Wrap,
 | 
			
		||||
                ..default()
 | 
			
		||||
            },
 | 
			
		||||
            background_color: PALETTE[0].into(),
 | 
			
		||||
            border_color: PALETTE[1].into(),
 | 
			
		||||
            ..default()
 | 
			
		||||
        })
 | 
			
		||||
        .with_children(|builder| {
 | 
			
		||||
            builder.spawn(NodeBundle {
 | 
			
		||||
                style: Style {
 | 
			
		||||
                    width: Val::Vw(30.),
 | 
			
		||||
                    height: Val::Vh(30.),
 | 
			
		||||
                    border: UiRect::all(Val::VMin(5.)),
 | 
			
		||||
                    ..default()
 | 
			
		||||
                },
 | 
			
		||||
                background_color: PALETTE[2].into(),
 | 
			
		||||
                border_color: PALETTE[9].into(),
 | 
			
		||||
                ..default()
 | 
			
		||||
            });
 | 
			
		||||
 | 
			
		||||
            builder.spawn(NodeBundle {
 | 
			
		||||
                style: Style {
 | 
			
		||||
                    width: Val::Vw(60.),
 | 
			
		||||
                    height: Val::Vh(30.),
 | 
			
		||||
                    ..default()
 | 
			
		||||
                },
 | 
			
		||||
                background_color: PALETTE[3].into(),
 | 
			
		||||
                ..default()
 | 
			
		||||
            });
 | 
			
		||||
 | 
			
		||||
            builder.spawn(NodeBundle {
 | 
			
		||||
                style: Style {
 | 
			
		||||
                    width: Val::Vw(45.),
 | 
			
		||||
                    height: Val::Vh(30.),
 | 
			
		||||
                    border: UiRect::left(Val::VMax(45. / 2.)),
 | 
			
		||||
                    ..default()
 | 
			
		||||
                },
 | 
			
		||||
                background_color: PALETTE[4].into(),
 | 
			
		||||
                border_color: PALETTE[8].into(),
 | 
			
		||||
                ..default()
 | 
			
		||||
            });
 | 
			
		||||
 | 
			
		||||
            builder.spawn(NodeBundle {
 | 
			
		||||
                style: Style {
 | 
			
		||||
                    width: Val::Vw(45.),
 | 
			
		||||
                    height: Val::Vh(30.),
 | 
			
		||||
                    border: UiRect::right(Val::VMax(45. / 2.)),
 | 
			
		||||
                    ..default()
 | 
			
		||||
                },
 | 
			
		||||
                background_color: PALETTE[5].into(),
 | 
			
		||||
                border_color: PALETTE[8].into(),
 | 
			
		||||
                ..default()
 | 
			
		||||
            });
 | 
			
		||||
 | 
			
		||||
            builder.spawn(NodeBundle {
 | 
			
		||||
                style: Style {
 | 
			
		||||
                    width: Val::Vw(60.),
 | 
			
		||||
                    height: Val::Vh(30.),
 | 
			
		||||
                    ..default()
 | 
			
		||||
                },
 | 
			
		||||
                background_color: PALETTE[6].into(),
 | 
			
		||||
                ..default()
 | 
			
		||||
            });
 | 
			
		||||
 | 
			
		||||
            builder.spawn(NodeBundle {
 | 
			
		||||
                style: Style {
 | 
			
		||||
                    width: Val::Vw(30.),
 | 
			
		||||
                    height: Val::Vh(30.),
 | 
			
		||||
                    border: UiRect::all(Val::VMin(5.)),
 | 
			
		||||
                    ..default()
 | 
			
		||||
                },
 | 
			
		||||
                background_color: PALETTE[7].into(),
 | 
			
		||||
                border_color: PALETTE[9].into(),
 | 
			
		||||
                ..default()
 | 
			
		||||
            });
 | 
			
		||||
        });
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
fn spawn_with_pixel_coords(mut commands: Commands) {
 | 
			
		||||
    commands
 | 
			
		||||
        .spawn(NodeBundle {
 | 
			
		||||
            style: Style {
 | 
			
		||||
                width: Val::Px(800.),
 | 
			
		||||
                height: Val::Px(600.),
 | 
			
		||||
                border: UiRect::axes(Val::Px(40.), Val::Px(30.)),
 | 
			
		||||
                flex_wrap: FlexWrap::Wrap,
 | 
			
		||||
                ..default()
 | 
			
		||||
            },
 | 
			
		||||
            background_color: PALETTE[1].into(),
 | 
			
		||||
            border_color: PALETTE[0].into(),
 | 
			
		||||
            ..default()
 | 
			
		||||
        })
 | 
			
		||||
        .with_children(|builder| {
 | 
			
		||||
            builder.spawn(NodeBundle {
 | 
			
		||||
                style: Style {
 | 
			
		||||
                    width: Val::Px(240.),
 | 
			
		||||
                    height: Val::Px(180.),
 | 
			
		||||
                    border: UiRect::axes(Val::Px(30.), Val::Px(30.)),
 | 
			
		||||
                    ..default()
 | 
			
		||||
                },
 | 
			
		||||
                background_color: PALETTE[2].into(),
 | 
			
		||||
                border_color: PALETTE[9].into(),
 | 
			
		||||
                ..default()
 | 
			
		||||
            });
 | 
			
		||||
 | 
			
		||||
            builder.spawn(NodeBundle {
 | 
			
		||||
                style: Style {
 | 
			
		||||
                    width: Val::Px(480.),
 | 
			
		||||
                    height: Val::Px(180.),
 | 
			
		||||
                    ..default()
 | 
			
		||||
                },
 | 
			
		||||
                background_color: PALETTE[3].into(),
 | 
			
		||||
                ..default()
 | 
			
		||||
            });
 | 
			
		||||
 | 
			
		||||
            builder.spawn(NodeBundle {
 | 
			
		||||
                style: Style {
 | 
			
		||||
                    width: Val::Px(360.),
 | 
			
		||||
                    height: Val::Px(180.),
 | 
			
		||||
                    border: UiRect::left(Val::Px(180.)),
 | 
			
		||||
                    ..default()
 | 
			
		||||
                },
 | 
			
		||||
                background_color: PALETTE[4].into(),
 | 
			
		||||
                border_color: PALETTE[8].into(),
 | 
			
		||||
                ..default()
 | 
			
		||||
            });
 | 
			
		||||
 | 
			
		||||
            builder.spawn(NodeBundle {
 | 
			
		||||
                style: Style {
 | 
			
		||||
                    width: Val::Px(360.),
 | 
			
		||||
                    height: Val::Px(180.),
 | 
			
		||||
                    border: UiRect::right(Val::Px(180.)),
 | 
			
		||||
                    ..default()
 | 
			
		||||
                },
 | 
			
		||||
                background_color: PALETTE[5].into(),
 | 
			
		||||
                border_color: PALETTE[8].into(),
 | 
			
		||||
                ..default()
 | 
			
		||||
            });
 | 
			
		||||
 | 
			
		||||
            builder.spawn(NodeBundle {
 | 
			
		||||
                style: Style {
 | 
			
		||||
                    width: Val::Px(480.),
 | 
			
		||||
                    height: Val::Px(180.),
 | 
			
		||||
                    ..default()
 | 
			
		||||
                },
 | 
			
		||||
                background_color: PALETTE[6].into(),
 | 
			
		||||
                ..default()
 | 
			
		||||
            });
 | 
			
		||||
 | 
			
		||||
            builder.spawn(NodeBundle {
 | 
			
		||||
                style: Style {
 | 
			
		||||
                    width: Val::Px(240.),
 | 
			
		||||
                    height: Val::Px(180.),
 | 
			
		||||
                    border: UiRect::axes(Val::Px(30.), Val::Px(30.)),
 | 
			
		||||
                    ..default()
 | 
			
		||||
                },
 | 
			
		||||
                background_color: PALETTE[7].into(),
 | 
			
		||||
                border_color: PALETTE[9].into(),
 | 
			
		||||
                ..default()
 | 
			
		||||
            });
 | 
			
		||||
        });
 | 
			
		||||
}
 | 
			
		||||
		Loading…
	
		Reference in New Issue
	
	Block a user