Start work on core menus
This commit is contained in:
parent
2d02f629b3
commit
588989a4d0
78
crates/bevy_core_widgets/src/core_menu.rs
Normal file
78
crates/bevy_core_widgets/src/core_menu.rs
Normal file
@ -0,0 +1,78 @@
|
||||
//! Core widget components for menus and menu buttons.
|
||||
|
||||
use accesskit::Role;
|
||||
use bevy_a11y::AccessibilityNode;
|
||||
use bevy_ecs::{
|
||||
component::Component,
|
||||
entity::Entity,
|
||||
event::{EntityEvent, Event},
|
||||
system::SystemId,
|
||||
traversal::Traversal,
|
||||
};
|
||||
|
||||
use crate::portal::{PortalTraversal, PortalTraversalItem};
|
||||
|
||||
/// Event use to control the state of the open menu. This bubbles upwards from the menu items
|
||||
/// and the menu container, through the portal relation, and to the menu owner entity.
|
||||
///
|
||||
/// 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.
|
||||
#[derive(Event, EntityEvent, Clone)]
|
||||
pub enum MenuEvent {
|
||||
/// Indicates we want to open the menu, if it is not already open.
|
||||
Open,
|
||||
/// Close the menu and despawn it. Despawning may not happen immediately if there is a closing
|
||||
/// transition animation.
|
||||
Close,
|
||||
/// 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 previous child in the parent's hierarchy (Shift-Tab).
|
||||
FocusPrev,
|
||||
/// Move the input focus to the next child in the parent's hierarchy (Tab).
|
||||
FocusNext,
|
||||
/// Move the input focus up (Arrow-Up).
|
||||
FocusUp,
|
||||
/// Move the input focus down (Arrow-Down).
|
||||
FocusDown,
|
||||
/// Move the input focus left (Arrow-Left).
|
||||
FocusLeft,
|
||||
/// Move the input focus right (Arrow-Right).
|
||||
FocusRight,
|
||||
}
|
||||
|
||||
impl Traversal<MenuEvent> for PortalTraversal {
|
||||
fn traverse(item: Self::Item<'_, '_>, _event: &MenuEvent) -> Option<Entity> {
|
||||
let PortalTraversalItem {
|
||||
child_of,
|
||||
portal_child_of,
|
||||
} = item;
|
||||
|
||||
// Send event to portal parent, if it has one.
|
||||
if let Some(portal_child_of) = portal_child_of {
|
||||
return Some(portal_child_of.parent());
|
||||
};
|
||||
|
||||
// Send event to parent, if it has one.
|
||||
if let Some(child_of) = child_of {
|
||||
return Some(child_of.parent());
|
||||
};
|
||||
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
/// Component that defines a popup menu
|
||||
#[derive(Component, Debug)]
|
||||
#[require(AccessibilityNode(accesskit::Node::new(Role::MenuListPopup)))]
|
||||
pub struct CoreMenuPopup;
|
||||
|
||||
/// 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.
|
||||
pub on_click: Option<SystemId>,
|
||||
}
|
||||
250
crates/bevy_core_widgets/src/floating.rs
Normal file
250
crates/bevy_core_widgets/src/floating.rs
Normal file
@ -0,0 +1,250 @@
|
||||
//! 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_math::{Rect, Vec2};
|
||||
use bevy_ui::{ComputedNode, ComputedNodeTarget, Node, UiGlobalTransform, Val};
|
||||
|
||||
/// Which side of the anchor element the floating element should be placed.
|
||||
#[derive(Debug, Default, Clone, Copy, PartialEq)]
|
||||
pub enum FloatSide {
|
||||
/// The floating element should be placed above the anchor.
|
||||
Top,
|
||||
/// The floating element should be placed below the anchor.
|
||||
#[default]
|
||||
Bottom,
|
||||
/// The floating element should be placed to the left of the anchor.
|
||||
Left,
|
||||
/// The floating element should be placed to the right of the anchor.
|
||||
Right,
|
||||
}
|
||||
|
||||
impl FloatSide {
|
||||
/// Returns the side that is the mirror image of this side.
|
||||
pub fn mirror(&self) -> Self {
|
||||
match self {
|
||||
FloatSide::Top => FloatSide::Bottom,
|
||||
FloatSide::Bottom => FloatSide::Top,
|
||||
FloatSide::Left => FloatSide::Right,
|
||||
FloatSide::Right => FloatSide::Left,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// How the floating element should be aligned to the anchor element. The alignment will be along an
|
||||
/// axis that is perpendicular to the direction of the float side. So for example, if the popup is
|
||||
/// positioned below the anchor, then the [`FloatAlign`] variant controls the horizontal aligment of
|
||||
/// the popup.
|
||||
#[derive(Debug, Default, Clone, Copy, PartialEq)]
|
||||
pub enum FloatAlign {
|
||||
/// The starting edge of the floating element should be aligned to the starting edge of the
|
||||
/// anchor.
|
||||
#[default]
|
||||
Start,
|
||||
/// The ending edge of the floating element should be aligned to the ending edge of the anchor.
|
||||
End,
|
||||
/// The center of the floating element should be aligned to the center of the anchor.
|
||||
Center,
|
||||
}
|
||||
|
||||
/// Indicates a possible position of a floating element relative to an anchor element. You can
|
||||
/// specify multiple possible positions; the positioning code will check to see if there is
|
||||
/// sufficient space to display the popup without clipping. If any position has sufficient room,
|
||||
/// it will pick the first one; if there are none, then it will pick the least bad one.
|
||||
#[derive(Debug, Default, Clone, Copy, PartialEq)]
|
||||
pub struct FloatPosition {
|
||||
/// The side of the anchor the floating element should be placed.
|
||||
pub side: FloatSide,
|
||||
|
||||
/// How the floating element should be aligned to the anchor.
|
||||
pub align: FloatAlign,
|
||||
|
||||
/// If true, the floating element will be at least as large as the anchor on the adjacent
|
||||
/// side.
|
||||
pub stretch: bool,
|
||||
|
||||
/// The size of the gap between the anchor and the floating element. This will offset the
|
||||
/// float along the direction of the [`FloatSide`].
|
||||
pub gap: f32,
|
||||
}
|
||||
|
||||
/// Defines the anchor position which the floating element is positioned relative to.
|
||||
#[derive(Debug, Clone, Copy, PartialEq)]
|
||||
pub enum FloatAnchor {
|
||||
/// The anchor is an entity with a UI [`Node`] component.
|
||||
Node(Entity),
|
||||
/// The anchor is an arbitrary rectangle in window coordinates.
|
||||
Rect(Rect),
|
||||
}
|
||||
|
||||
/// Component which is inserted into a floating element to make it dynamically position relative to
|
||||
/// an anchor element.
|
||||
#[derive(Component, PartialEq)]
|
||||
pub struct Floating {
|
||||
/// The entity that this floating element is anchored to.
|
||||
pub anchor: FloatAnchor,
|
||||
|
||||
/// List of potential positions for the floating element relative to the anchor.
|
||||
pub positions: Vec<FloatPosition>,
|
||||
}
|
||||
|
||||
impl Clone for Floating {
|
||||
fn clone(&self) -> Self {
|
||||
Self {
|
||||
anchor: self.anchor,
|
||||
positions: self.positions.clone(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn position_floating(
|
||||
mut q_float: Query<(&mut Node, &ComputedNode, &ComputedNodeTarget, &Floating)>,
|
||||
q_anchor: Query<(&ComputedNode, &UiGlobalTransform), Without<Floating>>,
|
||||
) {
|
||||
for (mut node, computed_node, computed_target, floating) in q_float.iter_mut() {
|
||||
// A rectangle which represents the area of the window.
|
||||
let window_rect = Rect {
|
||||
min: Vec2::ZERO,
|
||||
max: computed_target.logical_size(),
|
||||
};
|
||||
|
||||
// Compute the anchor rectangle.
|
||||
let anchor_rect: Rect = match floating.anchor {
|
||||
FloatAnchor::Node(anchor_entity) => {
|
||||
let Ok((anchor_node, anchor_transform)) = q_anchor.get(anchor_entity) else {
|
||||
continue;
|
||||
};
|
||||
Rect::from_center_size(anchor_transform.translation, anchor_node.size())
|
||||
}
|
||||
FloatAnchor::Rect(rect) => rect,
|
||||
};
|
||||
|
||||
let mut best_occluded = f32::MAX;
|
||||
let mut best_rect = Rect::default();
|
||||
let mut best_position: FloatPosition = Default::default();
|
||||
|
||||
// Loop through all the potential positions and find a good one.
|
||||
for position in &floating.positions {
|
||||
let float_size = computed_node.size();
|
||||
let mut rect = Rect::default();
|
||||
|
||||
// Taraget width and height depends on whether 'stretch' is true.
|
||||
let target_width = if position.stretch && position.side == FloatSide::Top
|
||||
|| position.side == FloatSide::Bottom
|
||||
{
|
||||
float_size.x.max(anchor_rect.width())
|
||||
} else {
|
||||
float_size.x
|
||||
};
|
||||
|
||||
let target_height = if position.stretch && position.side == FloatSide::Left
|
||||
|| position.side == FloatSide::Right
|
||||
{
|
||||
float_size.y.max(anchor_rect.height())
|
||||
} else {
|
||||
float_size.y
|
||||
};
|
||||
|
||||
// Position along main axis.
|
||||
match position.side {
|
||||
FloatSide::Top => {
|
||||
rect.max.y = anchor_rect.min.y - position.gap;
|
||||
rect.min.y = rect.max.y - float_size.y;
|
||||
}
|
||||
|
||||
FloatSide::Bottom => {
|
||||
rect.min.y = anchor_rect.max.y + position.gap;
|
||||
rect.max.y = rect.min.y + float_size.y;
|
||||
}
|
||||
|
||||
FloatSide::Left => {
|
||||
rect.max.x = anchor_rect.min.x - position.gap;
|
||||
rect.min.x = rect.max.x - float_size.x;
|
||||
}
|
||||
|
||||
FloatSide::Right => {
|
||||
rect.min.x = anchor_rect.max.x + position.gap;
|
||||
rect.max.x = rect.min.x + float_size.x;
|
||||
}
|
||||
}
|
||||
|
||||
// Position along secondary axis.
|
||||
match position.align {
|
||||
FloatAlign::Start => match position.side {
|
||||
FloatSide::Top | FloatSide::Bottom => {
|
||||
rect.min.x = anchor_rect.min.x;
|
||||
rect.max.x = rect.min.x + target_width;
|
||||
}
|
||||
|
||||
FloatSide::Left | FloatSide::Right => {
|
||||
rect.min.y = anchor_rect.min.y;
|
||||
rect.max.y = rect.min.y + target_height;
|
||||
}
|
||||
},
|
||||
|
||||
FloatAlign::End => match position.side {
|
||||
FloatSide::Top | FloatSide::Bottom => {
|
||||
rect.max.x = anchor_rect.max.x;
|
||||
rect.min.x = rect.max.x - target_width;
|
||||
}
|
||||
|
||||
FloatSide::Left | FloatSide::Right => {
|
||||
rect.max.y = anchor_rect.max.y;
|
||||
rect.min.y = rect.max.y - target_height;
|
||||
}
|
||||
},
|
||||
|
||||
FloatAlign::Center => match position.side {
|
||||
FloatSide::Top | FloatSide::Bottom => {
|
||||
rect.min.x = (anchor_rect.width() - target_width) * 0.5;
|
||||
rect.max.x = rect.min.x + target_width;
|
||||
}
|
||||
|
||||
FloatSide::Left | FloatSide::Right => {
|
||||
rect.min.y = (anchor_rect.width() - target_height) * 0.5;
|
||||
rect.max.y = rect.min.y + target_height;
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
// Clip to window and see how much of the floating element is occluded. We can calculate
|
||||
// how much was clipped by intersecting the rectangle against the window bounds, and
|
||||
// then subtracting the area from the area of the unclipped rectangle.
|
||||
let clipped_rect = rect.intersect(window_rect);
|
||||
let occlusion =
|
||||
rect.width() * rect.height() - clipped_rect.width() * clipped_rect.height();
|
||||
|
||||
// Find the position that has the least occlusion.
|
||||
if occlusion < best_occluded {
|
||||
best_occluded = occlusion;
|
||||
best_rect = rect;
|
||||
best_position = *position;
|
||||
}
|
||||
}
|
||||
|
||||
if best_occluded < f32::MAX {
|
||||
node.left = Val::Px(best_rect.min.x);
|
||||
node.top = Val::Px(best_rect.min.y);
|
||||
if best_position.stretch {
|
||||
match best_position.side {
|
||||
FloatSide::Top | FloatSide::Bottom => {
|
||||
node.min_width = Val::Px(best_rect.width());
|
||||
}
|
||||
|
||||
FloatSide::Left | FloatSide::Right => {
|
||||
node.min_height = Val::Px(best_rect.height());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Plugin that adds systems for the [`Floating`] component.
|
||||
pub struct FloatingPlugin;
|
||||
|
||||
impl Plugin for FloatingPlugin {
|
||||
fn build(&self, app: &mut App) {
|
||||
app.add_systems(PostUpdate, position_floating);
|
||||
}
|
||||
}
|
||||
@ -17,9 +17,12 @@
|
||||
mod callback;
|
||||
mod core_button;
|
||||
mod core_checkbox;
|
||||
mod core_menu;
|
||||
mod core_radio;
|
||||
mod core_scrollbar;
|
||||
mod core_slider;
|
||||
pub mod floating;
|
||||
pub mod portal;
|
||||
|
||||
use bevy_app::{App, Plugin};
|
||||
|
||||
@ -36,6 +39,8 @@ pub use core_slider::{
|
||||
SliderRange, SliderStep, SliderValue, TrackClick,
|
||||
};
|
||||
|
||||
use crate::floating::FloatingPlugin;
|
||||
|
||||
/// A plugin that registers the observers for all of the core widgets. If you don't want to
|
||||
/// use all of the widgets, you can import the individual widget plugins instead.
|
||||
pub struct CoreWidgetsPlugin;
|
||||
@ -43,6 +48,7 @@ pub struct CoreWidgetsPlugin;
|
||||
impl Plugin for CoreWidgetsPlugin {
|
||||
fn build(&self, app: &mut App) {
|
||||
app.add_plugins((
|
||||
FloatingPlugin,
|
||||
CoreButtonPlugin,
|
||||
CoreCheckboxPlugin,
|
||||
CoreRadioGroupPlugin,
|
||||
|
||||
34
crates/bevy_core_widgets/src/portal.rs
Normal file
34
crates/bevy_core_widgets/src/portal.rs
Normal file
@ -0,0 +1,34 @@
|
||||
//! 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.
|
||||
|
||||
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.
|
||||
#[derive(Component, Clone, PartialEq, Eq, Debug)]
|
||||
#[relationship(relationship_target = PortalChildren)]
|
||||
pub struct PortalChildOf(#[entities] pub Entity);
|
||||
|
||||
impl PortalChildOf {
|
||||
/// The parent entity of this child entity.
|
||||
#[inline]
|
||||
pub fn parent(&self) -> Entity {
|
||||
self.0
|
||||
}
|
||||
}
|
||||
|
||||
/// Tracks the portal children of this entity.
|
||||
#[derive(Component, Default, Debug, PartialEq, Eq)]
|
||||
#[relationship_target(relationship = PortalChildOf, linked_spawn)]
|
||||
pub struct PortalChildren(Vec<Entity>);
|
||||
|
||||
/// A traversal that uses either the [`ChildOf`] or [`PortalChildOf`] relationship. If the
|
||||
/// entity has both relations, the latter takes precedence.
|
||||
#[derive(QueryData)]
|
||||
pub struct PortalTraversal {
|
||||
pub(crate) child_of: Option<&'static ChildOf>,
|
||||
pub(crate) portal_child_of: Option<&'static PortalChildOf>,
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user