From 8ed8666cbfe707db778390b5cba9804bbdc309a1 Mon Sep 17 00:00:00 2001 From: Talin Date: Wed, 25 Jun 2025 14:30:38 -0700 Subject: [PATCH] More poking at `Floating`. --- crates/bevy_core_widgets/src/core_menu.rs | 13 ++-- crates/bevy_core_widgets/src/floating.rs | 26 +++++-- crates/bevy_core_widgets/src/portal.rs | 18 +++-- examples/ui/core_widgets.rs | 86 +++++++++++++++++++++++ 4 files changed, 127 insertions(+), 16 deletions(-) diff --git a/crates/bevy_core_widgets/src/core_menu.rs b/crates/bevy_core_widgets/src/core_menu.rs index 67d8871672..885f2081e2 100644 --- a/crates/bevy_core_widgets/src/core_menu.rs +++ b/crates/bevy_core_widgets/src/core_menu.rs @@ -17,7 +17,8 @@ use crate::portal::{PortalTraversal, PortalTraversalItem}; /// /// Focus navigation: the menu may be part of a composite of multiple menus such as a menu bar. /// This means that depending on direction, focus movement may move to the next menu item, or -/// the next menu. +/// the next menu. This also means that different events will often be handled at different +/// levels of the hierarchy - some being handled by the popup, and some by the popup's owner. #[derive(Event, EntityEvent, Clone)] pub enum MenuEvent { /// Indicates we want to open the menu, if it is not already open. @@ -28,6 +29,10 @@ pub enum MenuEvent { /// Move the input focs to the parent element. This usually happens as the menu is closing, /// although will not happen if the close was a result of clicking on the background. FocusParent, + /// Move the input focus to the first child in the parent's hierarchy (Home). + FocusFirst, + /// Move the input focus to the last child in the parent's hierarchy (End). + FocusLast, /// Move the input focus to the previous child in the parent's hierarchy (Shift-Tab). FocusPrev, /// Move the input focus to the next child in the parent's hierarchy (Tab). @@ -63,16 +68,16 @@ impl Traversal for PortalTraversal { } } -/// Component that defines a popup menu +/// Component that defines a popup menu container. #[derive(Component, Debug)] #[require(AccessibilityNode(accesskit::Node::new(Role::MenuListPopup)))] pub struct CoreMenuPopup; -/// Component that defines a menu item +/// Component that defines a menu item. #[derive(Component, Debug)] #[require(AccessibilityNode(accesskit::Node::new(Role::MenuItem)))] pub struct CoreMenuItem { /// Optional system to run when the menu item is clicked, or when the Enter or Space key - /// is pressed while the button is focused. + /// is pressed while the item is focused. pub on_click: Option, } diff --git a/crates/bevy_core_widgets/src/floating.rs b/crates/bevy_core_widgets/src/floating.rs index ce228b38de..6f6838842a 100644 --- a/crates/bevy_core_widgets/src/floating.rs +++ b/crates/bevy_core_widgets/src/floating.rs @@ -1,9 +1,14 @@ //! Framework for positioning of popups, tooltips, and other floating UI elements. -use bevy_app::{App, Plugin, PostUpdate}; -use bevy_ecs::{component::Component, entity::Entity, query::Without, system::Query}; +use bevy_app::{App, Plugin, PreUpdate}; +use bevy_ecs::{ + component::Component, entity::Entity, query::Without, schedule::IntoScheduleConfigs, + system::Query, +}; use bevy_math::{Rect, Vec2}; -use bevy_ui::{ComputedNode, ComputedNodeTarget, Node, UiGlobalTransform, Val}; +use bevy_ui::{ + ComputedNode, ComputedNodeTarget, Node, PositionType, UiGlobalTransform, UiSystems, Val, +}; /// Which side of the anchor element the floating element should be placed. #[derive(Debug, Default, Clone, Copy, PartialEq)] @@ -102,6 +107,11 @@ fn position_floating( q_anchor: Query<(&ComputedNode, &UiGlobalTransform), Without>, ) { for (mut node, computed_node, computed_target, floating) in q_float.iter_mut() { + // Logical size isn't set initially, ignore until it is. + if computed_target.logical_size().length_squared() == 0.0 { + continue; + } + // A rectangle which represents the area of the window. let window_rect = Rect { min: Vec2::ZERO, @@ -114,7 +124,10 @@ fn position_floating( let Ok((anchor_node, anchor_transform)) = q_anchor.get(anchor_entity) else { continue; }; - Rect::from_center_size(anchor_transform.translation, anchor_node.size()) + Rect::from_center_size( + anchor_transform.translation * anchor_node.inverse_scale_factor, + anchor_node.size() * anchor_node.inverse_scale_factor, + ) } FloatAnchor::Rect(rect) => rect, }; @@ -125,7 +138,7 @@ fn position_floating( // Loop through all the potential positions and find a good one. for position in &floating.positions { - let float_size = computed_node.size(); + let float_size = computed_node.size() * computed_node.inverse_scale_factor; let mut rect = Rect::default(); // Taraget width and height depends on whether 'stretch' is true. @@ -225,6 +238,7 @@ fn position_floating( if best_occluded < f32::MAX { node.left = Val::Px(best_rect.min.x); node.top = Val::Px(best_rect.min.y); + node.position_type = PositionType::Absolute; if best_position.stretch { match best_position.side { FloatSide::Top | FloatSide::Bottom => { @@ -245,6 +259,6 @@ pub struct FloatingPlugin; impl Plugin for FloatingPlugin { fn build(&self, app: &mut App) { - app.add_systems(PostUpdate, position_floating); + app.add_systems(PreUpdate, position_floating.in_set(UiSystems::Prepare)); } } diff --git a/crates/bevy_core_widgets/src/portal.rs b/crates/bevy_core_widgets/src/portal.rs index 50d6a55d29..4c5a8164b4 100644 --- a/crates/bevy_core_widgets/src/portal.rs +++ b/crates/bevy_core_widgets/src/portal.rs @@ -1,13 +1,19 @@ -//! Relationships for defining "portal children", where the term "portal" refers to a mechanism -//! whereby a logical child node can be physically located at a different point in the hierarchy. -//! The "portal" represents a logical connection between the child and it's parent which is not -//! a normal child relationship. +//! Relationships for defining "portal children". +//! +//! The term "portal" is commonly used in web user interface libraries to mean a mechanism whereby a +//! parent element can have a logical child which is physically present elsewhere in the hierarchy. +//! In this case, it means that for rendering and layout purposes, the child acts as a root node, +//! but for purposes of event bubbling and ownership, it acts as a child. +//! +//! This is typically used for UI elements such as menus and dialogs which need to calculate their +//! positions in window coordinates, despite being owned by UI elements nested deep within the +//! hierarchy. use bevy_ecs::{component::Component, entity::Entity, hierarchy::ChildOf, query::QueryData}; /// Defines the portal child relationship. For purposes of despawning, a portal child behaves /// as if it's a real child. However, for purpose of rendering and layout, a portal child behaves -/// as if it's a root element. Certain events can also bubble via the portal relationship. +/// as if it's a root element. Certain events can also bubble through the portal relationship. #[derive(Component, Clone, PartialEq, Eq, Debug)] #[relationship(relationship_target = PortalChildren)] pub struct PortalChildOf(#[entities] pub Entity); @@ -25,7 +31,7 @@ impl PortalChildOf { #[relationship_target(relationship = PortalChildOf, linked_spawn)] pub struct PortalChildren(Vec); -/// A traversal that uses either the [`ChildOf`] or [`PortalChildOf`] relationship. If the +/// A traversal algorithm that uses either the [`ChildOf`] or [`PortalChildOf`] relationship. If the /// entity has both relations, the latter takes precedence. #[derive(QueryData)] pub struct PortalTraversal { diff --git a/examples/ui/core_widgets.rs b/examples/ui/core_widgets.rs index 86aaa820f8..0398669e24 100644 --- a/examples/ui/core_widgets.rs +++ b/examples/ui/core_widgets.rs @@ -78,6 +78,10 @@ struct DemoCheckbox; #[derive(Component, Default)] struct DemoRadio(TrackClick); +/// Menuy button styling marker +#[derive(Component)] +struct DemoMenuButton; + /// A struct to hold the state of various widgets shown in the demo. /// /// While it is possible to use the widget's own state components as the source of truth, @@ -132,6 +136,8 @@ fn setup(mut commands: Commands, assets: Res) { }, ); + let on_open_menu = commands.register_system(spawn_popup); + // System to update a resource when the radio group changes. let on_change_radio = commands.register_system( |value: In, @@ -213,6 +219,49 @@ fn button(asset_server: &AssetServer, on_click: Callback) -> impl Bundle { ) } +fn menu_button(asset_server: &AssetServer, on_click: SystemId) -> impl Bundle { + ( + Node { + width: Val::Px(200.0), + height: Val::Px(65.0), + border: UiRect::all(Val::Px(5.0)), + justify_content: JustifyContent::SpaceBetween, + align_items: AlignItems::Center, + padding: UiRect::axes(Val::Px(16.0), Val::Px(0.0)), + ..default() + }, + DemoMenuButton, + CoreButton { + on_click: Callback::System(on_click), + }, + Hovered::default(), + TabIndex(0), + BorderColor::all(Color::BLACK), + BorderRadius::all(Val::Px(5.0)), + BackgroundColor(NORMAL_BUTTON), + children![ + ( + Text::new("Menu"), + TextFont { + font: asset_server.load("fonts/FiraSans-Bold.ttf"), + font_size: 33.0, + ..default() + }, + TextColor(Color::srgb(0.9, 0.9, 0.9)), + TextShadow::default(), + ), + ( + Node { + width: Val::Px(12.0), + height: Val::Px(12.0), + ..default() + }, + BackgroundColor(GRAY.into()), + ) + ], + ) +} + fn update_button_style( mut buttons: Query< ( @@ -739,6 +788,43 @@ fn radio(asset_server: &AssetServer, value: TrackClick, caption: &str) -> impl B ) } +fn spawn_popup(menu: Query>, mut commands: Commands) { + let Ok(anchor) = menu.single() else { + return; + }; + commands.entity(anchor).insert(PortalChildren::spawn_one(( + Node { + min_height: Val::Px(100.), + min_width: Val::Px(100.), + border: UiRect::all(Val::Px(2.0)), + position_type: PositionType::Absolute, + left: Val::Px(100.), + ..default() + }, + BorderColor::all(GREEN.into()), + BackgroundColor(GRAY.into()), + ZIndex(100), + Floating { + anchor: FloatAnchor::Node(anchor), + positions: vec![ + FloatPosition { + side: FloatSide::Bottom, + align: FloatAlign::Start, + gap: 2.0, + ..default() + }, + FloatPosition { + side: FloatSide::Top, + align: FloatAlign::Start, + gap: 2.0, + ..default() + }, + ], + }, + ))); + info!("Open menu"); +} + fn toggle_disabled( input: Res>, mut interaction_query: Query<