Renamed Floating to Popover; work on menus.
This commit is contained in:
parent
8ed8666cbf
commit
3c4fdd997c
@ -23,6 +23,7 @@ bevy_transform = { path = "../bevy_transform", version = "0.17.0-dev" }
|
|||||||
bevy_ui = { path = "../bevy_ui", version = "0.17.0-dev", features = [
|
bevy_ui = { path = "../bevy_ui", version = "0.17.0-dev", features = [
|
||||||
"bevy_ui_picking_backend",
|
"bevy_ui_picking_backend",
|
||||||
] }
|
] }
|
||||||
|
bevy_window = { path = "../bevy_window", version = "0.17.0-dev" }
|
||||||
|
|
||||||
# other
|
# other
|
||||||
accesskit = "0.19"
|
accesskit = "0.19"
|
||||||
|
@ -2,15 +2,30 @@
|
|||||||
|
|
||||||
use accesskit::Role;
|
use accesskit::Role;
|
||||||
use bevy_a11y::AccessibilityNode;
|
use bevy_a11y::AccessibilityNode;
|
||||||
|
use bevy_app::{App, Plugin};
|
||||||
use bevy_ecs::{
|
use bevy_ecs::{
|
||||||
component::Component,
|
component::Component,
|
||||||
entity::Entity,
|
entity::Entity,
|
||||||
event::{EntityEvent, Event},
|
event::{EntityEvent, Event},
|
||||||
system::SystemId,
|
hierarchy::ChildOf,
|
||||||
traversal::Traversal,
|
lifecycle::Add,
|
||||||
|
observer::On,
|
||||||
|
query::{Has, With},
|
||||||
|
system::{Commands, Query, ResMut},
|
||||||
};
|
};
|
||||||
|
use bevy_input::{
|
||||||
|
keyboard::{KeyCode, KeyboardInput},
|
||||||
|
ButtonState,
|
||||||
|
};
|
||||||
|
use bevy_input_focus::{
|
||||||
|
tab_navigation::{NavAction, TabGroup, TabNavigation},
|
||||||
|
AcquireFocus, FocusedInput, InputFocus,
|
||||||
|
};
|
||||||
|
use bevy_log::warn;
|
||||||
|
use bevy_ui::InteractionDisabled;
|
||||||
|
use bevy_window::PrimaryWindow;
|
||||||
|
|
||||||
use crate::portal::{PortalTraversal, PortalTraversalItem};
|
use crate::{Callback, Notify};
|
||||||
|
|
||||||
/// Event use to control the state of the open menu. This bubbles upwards from the menu items
|
/// 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.
|
/// and the menu container, through the portal relation, and to the menu owner entity.
|
||||||
@ -26,9 +41,10 @@ pub enum MenuEvent {
|
|||||||
/// Close the menu and despawn it. Despawning may not happen immediately if there is a closing
|
/// Close the menu and despawn it. Despawning may not happen immediately if there is a closing
|
||||||
/// transition animation.
|
/// transition animation.
|
||||||
Close,
|
Close,
|
||||||
/// Move the input focs to the parent element. This usually happens as the menu is closing,
|
/// Close the entire menu stack. The boolean argument indicates whether we want to retain
|
||||||
/// although will not happen if the close was a result of clicking on the background.
|
/// focus on the menu owner (the menu button). Whether this is true will depend on the reason
|
||||||
FocusParent,
|
/// for closing: a click on the background should not restore focus to the button.
|
||||||
|
CloseAll(bool),
|
||||||
/// Move the input focus to the first child in the parent's hierarchy (Home).
|
/// Move the input focus to the first child in the parent's hierarchy (Home).
|
||||||
FocusFirst,
|
FocusFirst,
|
||||||
/// Move the input focus to the last child in the parent's hierarchy (End).
|
/// Move the input focus to the last child in the parent's hierarchy (End).
|
||||||
@ -47,37 +63,173 @@ pub enum MenuEvent {
|
|||||||
FocusRight,
|
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 container.
|
/// Component that defines a popup menu container.
|
||||||
|
///
|
||||||
|
/// A popup menu *must* contain at least one focusable entity. The first such entity will acquire
|
||||||
|
/// focus when the popup is spawned; arrow keys can be used to navigate between menu items. If no
|
||||||
|
/// descendant of the menu has focus, the menu will automatically close. This rule has several
|
||||||
|
/// consequences:
|
||||||
|
///
|
||||||
|
/// * Clicking on another widget or empty space outside the menu will cause the menu to close.
|
||||||
|
/// * Two menus cannot be displayed at the same time unless one is an ancestor of the other.
|
||||||
#[derive(Component, Debug)]
|
#[derive(Component, Debug)]
|
||||||
#[require(AccessibilityNode(accesskit::Node::new(Role::MenuListPopup)))]
|
#[require(
|
||||||
|
AccessibilityNode(accesskit::Node::new(Role::MenuListPopup)),
|
||||||
|
TabGroup::modal()
|
||||||
|
)]
|
||||||
pub struct CoreMenuPopup;
|
pub struct CoreMenuPopup;
|
||||||
|
|
||||||
/// Component that defines a menu item.
|
/// Component that defines a menu item.
|
||||||
#[derive(Component, Debug)]
|
#[derive(Component, Debug)]
|
||||||
#[require(AccessibilityNode(accesskit::Node::new(Role::MenuItem)))]
|
#[require(AccessibilityNode(accesskit::Node::new(Role::MenuItem)))]
|
||||||
pub struct CoreMenuItem {
|
pub struct CoreMenuItem {
|
||||||
/// Optional system to run when the menu item is clicked, or when the Enter or Space key
|
/// Callback to invoke when the menu item is clicked, or when the `Enter` or `Space` key
|
||||||
/// is pressed while the item is focused.
|
/// is pressed while the item is focused.
|
||||||
pub on_click: Option<SystemId>,
|
pub on_activate: Callback,
|
||||||
|
}
|
||||||
|
|
||||||
|
fn menu_on_spawn(
|
||||||
|
ev: On<Add, CoreMenuPopup>,
|
||||||
|
mut focus: ResMut<InputFocus>,
|
||||||
|
tab_navigation: TabNavigation,
|
||||||
|
) {
|
||||||
|
// When a menu is spawned, attempt to find the first focusable menu item, and set focus
|
||||||
|
// to it.
|
||||||
|
if let Ok(next) = tab_navigation.initialize(ev.target(), NavAction::First) {
|
||||||
|
focus.0 = Some(next);
|
||||||
|
} else {
|
||||||
|
warn!("No focusable menu items for popup menu: {}", ev.target());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn menu_on_key_event(
|
||||||
|
mut ev: On<FocusedInput<KeyboardInput>>,
|
||||||
|
q_item: Query<(&CoreMenuItem, Has<InteractionDisabled>)>,
|
||||||
|
q_menu: Query<&CoreMenuPopup>,
|
||||||
|
mut commands: Commands,
|
||||||
|
) {
|
||||||
|
if let Ok((menu_item, disabled)) = q_item.get(ev.target()) {
|
||||||
|
if !disabled {
|
||||||
|
let event = &ev.event().input;
|
||||||
|
if !event.repeat && event.state == ButtonState::Pressed {
|
||||||
|
match event.key_code {
|
||||||
|
// Activate the item and close the popup
|
||||||
|
KeyCode::Enter | KeyCode::Space => {
|
||||||
|
ev.propagate(false);
|
||||||
|
commands.notify(&menu_item.on_activate);
|
||||||
|
commands.trigger_targets(MenuEvent::CloseAll(true), ev.target());
|
||||||
|
}
|
||||||
|
|
||||||
|
_ => (),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if let Ok(menu) = q_menu.get(ev.target()) {
|
||||||
|
let event = &ev.event().input;
|
||||||
|
if !event.repeat && event.state == ButtonState::Pressed {
|
||||||
|
match event.key_code {
|
||||||
|
// Close the popup
|
||||||
|
KeyCode::Escape => {
|
||||||
|
ev.propagate(false);
|
||||||
|
commands.trigger_targets(MenuEvent::CloseAll(true), ev.target());
|
||||||
|
}
|
||||||
|
|
||||||
|
// Focus the adjacent item in the up direction
|
||||||
|
KeyCode::ArrowUp => {
|
||||||
|
ev.propagate(false);
|
||||||
|
commands.trigger_targets(MenuEvent::FocusUp, ev.target());
|
||||||
|
}
|
||||||
|
|
||||||
|
// Focus the adjacent item in the down direction
|
||||||
|
KeyCode::ArrowDown => {
|
||||||
|
ev.propagate(false);
|
||||||
|
commands.trigger_targets(MenuEvent::FocusDown, ev.target());
|
||||||
|
}
|
||||||
|
|
||||||
|
// Focus the adjacent item in the left direction
|
||||||
|
KeyCode::ArrowLeft => {
|
||||||
|
ev.propagate(false);
|
||||||
|
commands.trigger_targets(MenuEvent::FocusLeft, ev.target());
|
||||||
|
}
|
||||||
|
|
||||||
|
// Focus the adjacent item in the right direction
|
||||||
|
KeyCode::ArrowRight => {
|
||||||
|
ev.propagate(false);
|
||||||
|
commands.trigger_targets(MenuEvent::FocusRight, ev.target());
|
||||||
|
}
|
||||||
|
|
||||||
|
// Focus the first item
|
||||||
|
KeyCode::Home => {
|
||||||
|
ev.propagate(false);
|
||||||
|
commands.trigger_targets(MenuEvent::FocusFirst, ev.target());
|
||||||
|
}
|
||||||
|
|
||||||
|
// Focus the last item
|
||||||
|
KeyCode::End => {
|
||||||
|
ev.propagate(false);
|
||||||
|
commands.trigger_targets(MenuEvent::FocusLast, ev.target());
|
||||||
|
}
|
||||||
|
|
||||||
|
_ => (),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn menu_on_menu_event(
|
||||||
|
mut ev: On<MenuEvent>,
|
||||||
|
q_popup: Query<(), With<CoreMenuPopup>>,
|
||||||
|
q_parent: Query<&ChildOf>,
|
||||||
|
windows: Query<Entity, With<PrimaryWindow>>,
|
||||||
|
mut commands: Commands,
|
||||||
|
) {
|
||||||
|
if q_popup.contains(ev.target()) {
|
||||||
|
match ev.event() {
|
||||||
|
MenuEvent::Open => todo!(),
|
||||||
|
MenuEvent::Close => {
|
||||||
|
ev.propagate(false);
|
||||||
|
commands.entity(ev.target()).despawn();
|
||||||
|
}
|
||||||
|
MenuEvent::CloseAll(retain_focus) => {
|
||||||
|
// For CloseAll, find the root menu popup and despawn it
|
||||||
|
// This will propagate the despawn to all child popups
|
||||||
|
let root_menu = q_parent
|
||||||
|
.iter_ancestors(ev.target())
|
||||||
|
.filter(|&e| q_popup.contains(e))
|
||||||
|
.last()
|
||||||
|
.unwrap_or(ev.target());
|
||||||
|
|
||||||
|
// Get the parent of the root menu and trigger an AcquireFocus event.
|
||||||
|
if let Ok(root_parent) = q_parent.get(root_menu) {
|
||||||
|
if *retain_focus {
|
||||||
|
if let Ok(window) = windows.single() {
|
||||||
|
commands.trigger_targets(AcquireFocus { window }, root_parent.parent());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ev.propagate(false);
|
||||||
|
commands.entity(root_menu).despawn();
|
||||||
|
}
|
||||||
|
MenuEvent::FocusFirst => todo!(),
|
||||||
|
MenuEvent::FocusLast => todo!(),
|
||||||
|
MenuEvent::FocusPrev => todo!(),
|
||||||
|
MenuEvent::FocusNext => todo!(),
|
||||||
|
MenuEvent::FocusUp => todo!(),
|
||||||
|
MenuEvent::FocusDown => todo!(),
|
||||||
|
MenuEvent::FocusLeft => todo!(),
|
||||||
|
MenuEvent::FocusRight => todo!(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Plugin that adds the observers for the [`CoreButton`] widget.
|
||||||
|
pub struct CoreMenuPlugin;
|
||||||
|
|
||||||
|
impl Plugin for CoreMenuPlugin {
|
||||||
|
fn build(&self, app: &mut App) {
|
||||||
|
app.add_observer(menu_on_spawn)
|
||||||
|
.add_observer(menu_on_key_event)
|
||||||
|
.add_observer(menu_on_menu_event);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,264 +0,0 @@
|
|||||||
//! Framework for positioning of popups, tooltips, and other floating UI elements.
|
|
||||||
|
|
||||||
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, PositionType, UiGlobalTransform, UiSystems, 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() {
|
|
||||||
// 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,
|
|
||||||
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.inverse_scale_factor,
|
|
||||||
anchor_node.size() * anchor_node.inverse_scale_factor,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
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() * computed_node.inverse_scale_factor;
|
|
||||||
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);
|
|
||||||
node.position_type = PositionType::Absolute;
|
|
||||||
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(PreUpdate, position_floating.in_set(UiSystems::Prepare));
|
|
||||||
}
|
|
||||||
}
|
|
@ -21,14 +21,14 @@ mod core_menu;
|
|||||||
mod core_radio;
|
mod core_radio;
|
||||||
mod core_scrollbar;
|
mod core_scrollbar;
|
||||||
mod core_slider;
|
mod core_slider;
|
||||||
pub mod floating;
|
pub mod popover;
|
||||||
pub mod portal;
|
|
||||||
|
|
||||||
use bevy_app::{App, Plugin};
|
use bevy_app::{App, Plugin};
|
||||||
|
|
||||||
pub use callback::{Callback, Notify};
|
pub use callback::{Callback, Notify};
|
||||||
pub use core_button::{CoreButton, CoreButtonPlugin};
|
pub use core_button::{CoreButton, CoreButtonPlugin};
|
||||||
pub use core_checkbox::{CoreCheckbox, CoreCheckboxPlugin, SetChecked, ToggleChecked};
|
pub use core_checkbox::{CoreCheckbox, CoreCheckboxPlugin, SetChecked, ToggleChecked};
|
||||||
|
pub use core_menu::{CoreMenuItem, CoreMenuPlugin, CoreMenuPopup};
|
||||||
pub use core_radio::{CoreRadio, CoreRadioGroup, CoreRadioGroupPlugin};
|
pub use core_radio::{CoreRadio, CoreRadioGroup, CoreRadioGroupPlugin};
|
||||||
pub use core_scrollbar::{
|
pub use core_scrollbar::{
|
||||||
ControlOrientation, CoreScrollbar, CoreScrollbarDragState, CoreScrollbarPlugin,
|
ControlOrientation, CoreScrollbar, CoreScrollbarDragState, CoreScrollbarPlugin,
|
||||||
@ -39,7 +39,7 @@ pub use core_slider::{
|
|||||||
SliderRange, SliderStep, SliderValue, TrackClick,
|
SliderRange, SliderStep, SliderValue, TrackClick,
|
||||||
};
|
};
|
||||||
|
|
||||||
use crate::floating::FloatingPlugin;
|
use crate::popover::PopoverPlugin;
|
||||||
|
|
||||||
/// A plugin that registers the observers for all of the core widgets. If you don't want to
|
/// 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.
|
/// use all of the widgets, you can import the individual widget plugins instead.
|
||||||
@ -48,9 +48,10 @@ pub struct CoreWidgetsPlugin;
|
|||||||
impl Plugin for CoreWidgetsPlugin {
|
impl Plugin for CoreWidgetsPlugin {
|
||||||
fn build(&self, app: &mut App) {
|
fn build(&self, app: &mut App) {
|
||||||
app.add_plugins((
|
app.add_plugins((
|
||||||
FloatingPlugin,
|
PopoverPlugin,
|
||||||
CoreButtonPlugin,
|
CoreButtonPlugin,
|
||||||
CoreCheckboxPlugin,
|
CoreCheckboxPlugin,
|
||||||
|
CoreMenuPlugin,
|
||||||
CoreRadioGroupPlugin,
|
CoreRadioGroupPlugin,
|
||||||
CoreScrollbarPlugin,
|
CoreScrollbarPlugin,
|
||||||
CoreSliderPlugin,
|
CoreSliderPlugin,
|
||||||
|
246
crates/bevy_core_widgets/src/popover.rs
Normal file
246
crates/bevy_core_widgets/src/popover.rs
Normal file
@ -0,0 +1,246 @@
|
|||||||
|
//! Framework for positioning of popups, tooltips, and other popover UI elements.
|
||||||
|
|
||||||
|
use bevy_app::{App, Plugin, PreUpdate};
|
||||||
|
use bevy_ecs::{
|
||||||
|
change_detection::DetectChangesMut, component::Component, hierarchy::ChildOf, query::Without,
|
||||||
|
schedule::IntoScheduleConfigs, system::Query,
|
||||||
|
};
|
||||||
|
use bevy_math::{Rect, Vec2};
|
||||||
|
use bevy_render::view::Visibility;
|
||||||
|
use bevy_ui::{
|
||||||
|
ComputedNode, ComputedNodeTarget, Node, PositionType, UiGlobalTransform, UiSystems, Val,
|
||||||
|
};
|
||||||
|
|
||||||
|
/// Which side of the parent element the popover element should be placed.
|
||||||
|
#[derive(Debug, Default, Clone, Copy, PartialEq)]
|
||||||
|
pub enum PopoverSide {
|
||||||
|
/// The popover element should be placed above the parent.
|
||||||
|
Top,
|
||||||
|
/// The popover element should be placed below the parent.
|
||||||
|
#[default]
|
||||||
|
Bottom,
|
||||||
|
/// The popover element should be placed to the left of the parent.
|
||||||
|
Left,
|
||||||
|
/// The popover element should be placed to the right of the parent.
|
||||||
|
Right,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl PopoverSide {
|
||||||
|
/// Returns the side that is the mirror image of this side.
|
||||||
|
pub fn mirror(&self) -> Self {
|
||||||
|
match self {
|
||||||
|
PopoverSide::Top => PopoverSide::Bottom,
|
||||||
|
PopoverSide::Bottom => PopoverSide::Top,
|
||||||
|
PopoverSide::Left => PopoverSide::Right,
|
||||||
|
PopoverSide::Right => PopoverSide::Left,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// How the popover element should be aligned to the parent element. The alignment will be along an
|
||||||
|
/// axis that is perpendicular to the direction of the popover side. So for example, if the popup is
|
||||||
|
/// positioned below the parent, then the [`PopoverAlign`] variant controls the horizontal aligment
|
||||||
|
/// of the popup.
|
||||||
|
#[derive(Debug, Default, Clone, Copy, PartialEq)]
|
||||||
|
pub enum PopoverAlign {
|
||||||
|
/// The starting edge of the popover element should be aligned to the starting edge of the
|
||||||
|
/// parent.
|
||||||
|
#[default]
|
||||||
|
Start,
|
||||||
|
/// The ending edge of the popover element should be aligned to the ending edge of the parent.
|
||||||
|
End,
|
||||||
|
/// The center of the popover element should be aligned to the center of the parent.
|
||||||
|
Center,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Indicates a possible position of a popover element relative to it's parent. 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 PopoverPlacement {
|
||||||
|
/// The side of the parent entity where the popover element should be placed.
|
||||||
|
pub side: PopoverSide,
|
||||||
|
|
||||||
|
/// How the popover element should be aligned to the parent entity.
|
||||||
|
pub align: PopoverAlign,
|
||||||
|
|
||||||
|
/// The size of the gap between the parent and the popover element, in logical pixels. This will
|
||||||
|
/// offset the popover along the direction of [`side`].
|
||||||
|
pub gap: f32,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Component which is inserted into a popover element to make it dynamically position relative to
|
||||||
|
/// an parent element.
|
||||||
|
#[derive(Component, PartialEq)]
|
||||||
|
pub struct Popover {
|
||||||
|
/// List of potential positions for the popover element relative to the parent.
|
||||||
|
pub positions: Vec<PopoverPlacement>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Clone for Popover {
|
||||||
|
fn clone(&self) -> Self {
|
||||||
|
Self {
|
||||||
|
positions: self.positions.clone(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn position_popover(
|
||||||
|
mut q_popover: Query<(
|
||||||
|
&mut Node,
|
||||||
|
&mut Visibility,
|
||||||
|
&ComputedNode,
|
||||||
|
&ComputedNodeTarget,
|
||||||
|
&Popover,
|
||||||
|
&ChildOf,
|
||||||
|
)>,
|
||||||
|
q_parent: Query<(&ComputedNode, &UiGlobalTransform), Without<Popover>>,
|
||||||
|
) {
|
||||||
|
for (mut node, mut visibility, computed_node, computed_target, popover, parent) in
|
||||||
|
q_popover.iter_mut()
|
||||||
|
{
|
||||||
|
// A rectangle which represents the area of the window.
|
||||||
|
let window_rect = Rect {
|
||||||
|
min: Vec2::ZERO,
|
||||||
|
max: computed_target.logical_size(),
|
||||||
|
};
|
||||||
|
|
||||||
|
// Logical size isn't set initially, ignore until it is.
|
||||||
|
if window_rect.area() <= 0.0 {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Compute the parent rectangle.
|
||||||
|
let Ok((parent_node, parent_transform)) = q_parent.get(parent.parent()) else {
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
// Computed node size includes the border, but since absolute positioning doesn't include
|
||||||
|
// border we need to remove it from the calculations.
|
||||||
|
let parent_size = parent_node.size()
|
||||||
|
- Vec2::new(
|
||||||
|
parent_node.border.left + parent_node.border.right,
|
||||||
|
parent_node.border.top + parent_node.border.bottom,
|
||||||
|
);
|
||||||
|
let parent_rect = Rect::from_center_size(parent_transform.translation, parent_size)
|
||||||
|
.scale(parent_node.inverse_scale_factor);
|
||||||
|
|
||||||
|
let mut best_occluded = f32::MAX;
|
||||||
|
let mut best_rect = Rect::default();
|
||||||
|
|
||||||
|
// Loop through all the potential positions and find a good one.
|
||||||
|
for position in &popover.positions {
|
||||||
|
let popover_size = computed_node.size() * computed_node.inverse_scale_factor;
|
||||||
|
let mut rect = Rect::default();
|
||||||
|
|
||||||
|
let target_width = popover_size.x;
|
||||||
|
let target_height = popover_size.y;
|
||||||
|
|
||||||
|
// Position along main axis.
|
||||||
|
match position.side {
|
||||||
|
PopoverSide::Top => {
|
||||||
|
rect.max.y = parent_rect.min.y - position.gap;
|
||||||
|
rect.min.y = rect.max.y - popover_size.y;
|
||||||
|
}
|
||||||
|
|
||||||
|
PopoverSide::Bottom => {
|
||||||
|
rect.min.y = parent_rect.max.y + position.gap;
|
||||||
|
rect.max.y = rect.min.y + popover_size.y;
|
||||||
|
}
|
||||||
|
|
||||||
|
PopoverSide::Left => {
|
||||||
|
rect.max.x = parent_rect.min.x - position.gap;
|
||||||
|
rect.min.x = rect.max.x - popover_size.x;
|
||||||
|
}
|
||||||
|
|
||||||
|
PopoverSide::Right => {
|
||||||
|
rect.min.x = parent_rect.max.x + position.gap;
|
||||||
|
rect.max.x = rect.min.x + popover_size.x;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Position along secondary axis.
|
||||||
|
match position.align {
|
||||||
|
PopoverAlign::Start => match position.side {
|
||||||
|
PopoverSide::Top | PopoverSide::Bottom => {
|
||||||
|
rect.min.x = parent_rect.min.x;
|
||||||
|
rect.max.x = rect.min.x + target_width;
|
||||||
|
}
|
||||||
|
|
||||||
|
PopoverSide::Left | PopoverSide::Right => {
|
||||||
|
rect.min.y = parent_rect.min.y;
|
||||||
|
rect.max.y = rect.min.y + target_height;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
PopoverAlign::End => match position.side {
|
||||||
|
PopoverSide::Top | PopoverSide::Bottom => {
|
||||||
|
rect.max.x = parent_rect.max.x;
|
||||||
|
rect.min.x = rect.max.x - target_width;
|
||||||
|
}
|
||||||
|
|
||||||
|
PopoverSide::Left | PopoverSide::Right => {
|
||||||
|
rect.max.y = parent_rect.max.y;
|
||||||
|
rect.min.y = rect.max.y - target_height;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
PopoverAlign::Center => match position.side {
|
||||||
|
PopoverSide::Top | PopoverSide::Bottom => {
|
||||||
|
rect.min.x = (parent_rect.width() - target_width) * 0.5;
|
||||||
|
rect.max.x = rect.min.x + target_width;
|
||||||
|
}
|
||||||
|
|
||||||
|
PopoverSide::Left | PopoverSide::Right => {
|
||||||
|
rect.min.y = (parent_rect.width() - target_height) * 0.5;
|
||||||
|
rect.max.y = rect.min.y + target_height;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clip to window and see how much of the popover 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.area() - clipped_rect.area();
|
||||||
|
|
||||||
|
// Find the position that has the least occlusion.
|
||||||
|
if occlusion < best_occluded {
|
||||||
|
best_occluded = occlusion;
|
||||||
|
best_rect = rect;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update node properties, but only if they are different from before (to avoid setting
|
||||||
|
// change detection bit).
|
||||||
|
if best_occluded < f32::MAX {
|
||||||
|
let left = Val::Px(best_rect.min.x - parent_rect.min.x);
|
||||||
|
let top = Val::Px(best_rect.min.y - parent_rect.min.y);
|
||||||
|
visibility.set_if_neq(Visibility::Visible);
|
||||||
|
if node.left != left {
|
||||||
|
node.left = left;
|
||||||
|
}
|
||||||
|
if node.top != top {
|
||||||
|
node.top = top;
|
||||||
|
}
|
||||||
|
if node.bottom != Val::DEFAULT {
|
||||||
|
node.bottom = Val::DEFAULT;
|
||||||
|
}
|
||||||
|
if node.right != Val::DEFAULT {
|
||||||
|
node.right = Val::DEFAULT;
|
||||||
|
}
|
||||||
|
if node.position_type != PositionType::Absolute {
|
||||||
|
node.position_type = PositionType::Absolute;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Plugin that adds systems for the [`Popover`] component.
|
||||||
|
pub struct PopoverPlugin;
|
||||||
|
|
||||||
|
impl Plugin for PopoverPlugin {
|
||||||
|
fn build(&self, app: &mut App) {
|
||||||
|
app.add_systems(PreUpdate, position_popover.in_set(UiSystems::Prepare));
|
||||||
|
}
|
||||||
|
}
|
@ -1,40 +0,0 @@
|
|||||||
//! 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 through 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 algorithm 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>,
|
|
||||||
}
|
|
@ -153,7 +153,7 @@ pub struct FocusedInput<E: BufferedEvent + Clone> {
|
|||||||
#[entity_event(traversal = WindowTraversal, auto_propagate)]
|
#[entity_event(traversal = WindowTraversal, auto_propagate)]
|
||||||
pub struct AcquireFocus {
|
pub struct AcquireFocus {
|
||||||
/// The primary window entity.
|
/// The primary window entity.
|
||||||
window: Entity,
|
pub window: Entity,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(QueryData)]
|
#[derive(QueryData)]
|
||||||
|
@ -166,12 +166,12 @@ pub struct TabNavigation<'w, 's> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl TabNavigation<'_, '_> {
|
impl TabNavigation<'_, '_> {
|
||||||
/// Navigate to the desired focusable entity.
|
/// Navigate to the desired focusable entity, relative to the current focused entity.
|
||||||
///
|
///
|
||||||
/// Change the [`NavAction`] to navigate in a different direction.
|
/// Change the [`NavAction`] to navigate in a different direction.
|
||||||
/// Focusable entities are determined by the presence of the [`TabIndex`] component.
|
/// Focusable entities are determined by the presence of the [`TabIndex`] component.
|
||||||
///
|
///
|
||||||
/// If no focusable entities are found, then this function will return either the first
|
/// If there is no currently focused entity, then this function will return either the first
|
||||||
/// or last focusable entity, depending on the direction of navigation. For example, if
|
/// or last focusable entity, depending on the direction of navigation. For example, if
|
||||||
/// `action` is `Next` and no focusable entities are found, then this function will return
|
/// `action` is `Next` and no focusable entities are found, then this function will return
|
||||||
/// the first focusable entity.
|
/// the first focusable entity.
|
||||||
@ -198,13 +198,46 @@ impl TabNavigation<'_, '_> {
|
|||||||
})
|
})
|
||||||
});
|
});
|
||||||
|
|
||||||
|
self.navigate_internal(focus.0, action, tabgroup)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Initialize focus to a focusable child of a container, either the first or last
|
||||||
|
/// depending on [`NavAction`]. This assumes that the parent entity has a [`TabGroup`]
|
||||||
|
/// component.
|
||||||
|
///
|
||||||
|
/// Focusable entities are determined by the presence of the [`TabIndex`] component.
|
||||||
|
pub fn initialize(
|
||||||
|
&self,
|
||||||
|
parent: Entity,
|
||||||
|
action: NavAction,
|
||||||
|
) -> Result<Entity, TabNavigationError> {
|
||||||
|
// If there are no tab groups, then there are no focusable entities.
|
||||||
|
if self.tabgroup_query.is_empty() {
|
||||||
|
return Err(TabNavigationError::NoTabGroups);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Look for the tab group on the parent entity.
|
||||||
|
match self.tabgroup_query.get(parent) {
|
||||||
|
Ok(tabgroup) => self.navigate_internal(None, action, Some((parent, tabgroup.1))),
|
||||||
|
Err(_) => Err(TabNavigationError::NoTabGroups),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn navigate_internal(
|
||||||
|
&self,
|
||||||
|
focus: Option<Entity>,
|
||||||
|
action: NavAction,
|
||||||
|
tabgroup: Option<(Entity, &TabGroup)>,
|
||||||
|
) -> Result<Entity, TabNavigationError> {
|
||||||
let navigation_result = self.navigate_in_group(tabgroup, focus, action);
|
let navigation_result = self.navigate_in_group(tabgroup, focus, action);
|
||||||
|
|
||||||
match navigation_result {
|
match navigation_result {
|
||||||
Ok(entity) => {
|
Ok(entity) => {
|
||||||
if focus.0.is_some() && tabgroup.is_none() {
|
if let Some(previous_focus) = focus
|
||||||
|
&& tabgroup.is_none()
|
||||||
|
{
|
||||||
Err(TabNavigationError::NoTabGroupForCurrentFocus {
|
Err(TabNavigationError::NoTabGroupForCurrentFocus {
|
||||||
previous_focus: focus.0.unwrap(),
|
previous_focus,
|
||||||
new_focus: entity,
|
new_focus: entity,
|
||||||
})
|
})
|
||||||
} else {
|
} else {
|
||||||
@ -218,7 +251,7 @@ impl TabNavigation<'_, '_> {
|
|||||||
fn navigate_in_group(
|
fn navigate_in_group(
|
||||||
&self,
|
&self,
|
||||||
tabgroup: Option<(Entity, &TabGroup)>,
|
tabgroup: Option<(Entity, &TabGroup)>,
|
||||||
focus: &InputFocus,
|
focus: Option<Entity>,
|
||||||
action: NavAction,
|
action: NavAction,
|
||||||
) -> Result<Entity, TabNavigationError> {
|
) -> Result<Entity, TabNavigationError> {
|
||||||
// List of all focusable entities found.
|
// List of all focusable entities found.
|
||||||
@ -268,7 +301,7 @@ impl TabNavigation<'_, '_> {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
let index = focusable.iter().position(|e| Some(e.0) == focus.0);
|
let index = focusable.iter().position(|e| Some(e.0) == focus);
|
||||||
let count = focusable.len();
|
let count = focusable.len();
|
||||||
let next = match (index, action) {
|
let next = match (index, action) {
|
||||||
(Some(idx), NavAction::Next) => (idx + 1).rem_euclid(count),
|
(Some(idx), NavAction::Next) => (idx + 1).rem_euclid(count),
|
||||||
|
@ -356,6 +356,38 @@ impl Rect {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Return the area of this rectangle.
|
||||||
|
///
|
||||||
|
/// # Examples
|
||||||
|
///
|
||||||
|
/// ```
|
||||||
|
/// # use bevy_math::Rect;
|
||||||
|
/// let r = Rect::new(0., 0., 10., 10.); // w=10 h=10
|
||||||
|
/// assert_eq!(r.area(), 100.0);
|
||||||
|
/// ```
|
||||||
|
#[inline]
|
||||||
|
pub fn area(&self) -> f32 {
|
||||||
|
self.width() * self.height()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Scale this rect by a multiplicative factor
|
||||||
|
///
|
||||||
|
/// # Examples
|
||||||
|
///
|
||||||
|
/// ```
|
||||||
|
/// # use bevy_math::Rect;
|
||||||
|
/// let r = Rect::new(1., 1., 2., 2.); // w=10 h=10
|
||||||
|
/// assert_eq!(r.scale(2.).min.x, 2.0);
|
||||||
|
/// assert_eq!(r.scale(2.).max.x, 4.0);
|
||||||
|
/// ```
|
||||||
|
#[inline]
|
||||||
|
pub fn scale(&self, factor: f32) -> Rect {
|
||||||
|
Self {
|
||||||
|
min: self.min * factor,
|
||||||
|
max: self.max * factor,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Returns self as [`IRect`] (i32)
|
/// Returns self as [`IRect`] (i32)
|
||||||
#[inline]
|
#[inline]
|
||||||
pub fn as_irect(&self) -> IRect {
|
pub fn as_irect(&self) -> IRect {
|
||||||
|
@ -3,7 +3,8 @@
|
|||||||
use bevy::{
|
use bevy::{
|
||||||
color::palettes::basic::*,
|
color::palettes::basic::*,
|
||||||
core_widgets::{
|
core_widgets::{
|
||||||
Callback, CoreButton, CoreCheckbox, CoreRadio, CoreRadioGroup, CoreSlider,
|
popover::{Popover, PopoverAlign, PopoverPlacement, PopoverSide},
|
||||||
|
Callback, CoreButton, CoreCheckbox, CoreMenuPopup, CoreRadio, CoreRadioGroup, CoreSlider,
|
||||||
CoreSliderDragState, CoreSliderThumb, CoreWidgetsPlugin, SliderRange, SliderValue,
|
CoreSliderDragState, CoreSliderThumb, CoreWidgetsPlugin, SliderRange, SliderValue,
|
||||||
TrackClick,
|
TrackClick,
|
||||||
},
|
},
|
||||||
@ -26,7 +27,7 @@ fn main() {
|
|||||||
TabNavigationPlugin,
|
TabNavigationPlugin,
|
||||||
))
|
))
|
||||||
// Only run the app when there is user input. This will significantly reduce CPU/GPU use.
|
// Only run the app when there is user input. This will significantly reduce CPU/GPU use.
|
||||||
.insert_resource(WinitSettings::desktop_app())
|
// .insert_resource(WinitSettings::desktop_app())
|
||||||
.insert_resource(DemoWidgetStates {
|
.insert_resource(DemoWidgetStates {
|
||||||
slider_value: 50.0,
|
slider_value: 50.0,
|
||||||
slider_click: TrackClick::Snap,
|
slider_click: TrackClick::Snap,
|
||||||
@ -156,6 +157,7 @@ fn setup(mut commands: Commands, assets: Res<AssetServer>) {
|
|||||||
Callback::System(on_click),
|
Callback::System(on_click),
|
||||||
Callback::System(on_change_value),
|
Callback::System(on_change_value),
|
||||||
Callback::System(on_change_radio),
|
Callback::System(on_change_radio),
|
||||||
|
Callback::System(on_open_menu),
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -164,6 +166,7 @@ fn demo_root(
|
|||||||
on_click: Callback,
|
on_click: Callback,
|
||||||
on_change_value: Callback<In<f32>>,
|
on_change_value: Callback<In<f32>>,
|
||||||
on_change_radio: Callback<In<Entity>>,
|
on_change_radio: Callback<In<Entity>>,
|
||||||
|
on_open_menu: Callback,
|
||||||
) -> impl Bundle {
|
) -> impl Bundle {
|
||||||
(
|
(
|
||||||
Node {
|
Node {
|
||||||
@ -182,6 +185,7 @@ fn demo_root(
|
|||||||
slider(0.0, 100.0, 50.0, on_change_value),
|
slider(0.0, 100.0, 50.0, on_change_value),
|
||||||
checkbox(asset_server, "Checkbox", Callback::Ignore),
|
checkbox(asset_server, "Checkbox", Callback::Ignore),
|
||||||
radio_group(asset_server, on_change_radio),
|
radio_group(asset_server, on_change_radio),
|
||||||
|
menu_button(asset_server, on_open_menu),
|
||||||
Text::new("Press 'D' to toggle widget disabled states"),
|
Text::new("Press 'D' to toggle widget disabled states"),
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
@ -219,21 +223,20 @@ fn button(asset_server: &AssetServer, on_click: Callback) -> impl Bundle {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn menu_button(asset_server: &AssetServer, on_click: SystemId) -> impl Bundle {
|
fn menu_button(asset_server: &AssetServer, on_activate: Callback) -> impl Bundle {
|
||||||
(
|
(
|
||||||
Node {
|
Node {
|
||||||
width: Val::Px(200.0),
|
width: Val::Px(200.0),
|
||||||
height: Val::Px(65.0),
|
height: Val::Px(65.0),
|
||||||
border: UiRect::all(Val::Px(5.0)),
|
border: UiRect::all(Val::Px(5.0)),
|
||||||
|
box_sizing: BoxSizing::BorderBox,
|
||||||
justify_content: JustifyContent::SpaceBetween,
|
justify_content: JustifyContent::SpaceBetween,
|
||||||
align_items: AlignItems::Center,
|
align_items: AlignItems::Center,
|
||||||
padding: UiRect::axes(Val::Px(16.0), Val::Px(0.0)),
|
padding: UiRect::axes(Val::Px(16.0), Val::Px(0.0)),
|
||||||
..default()
|
..default()
|
||||||
},
|
},
|
||||||
DemoMenuButton,
|
DemoMenuButton,
|
||||||
CoreButton {
|
CoreButton { on_activate },
|
||||||
on_click: Callback::System(on_click),
|
|
||||||
},
|
|
||||||
Hovered::default(),
|
Hovered::default(),
|
||||||
TabIndex(0),
|
TabIndex(0),
|
||||||
BorderColor::all(Color::BLACK),
|
BorderColor::all(Color::BLACK),
|
||||||
@ -792,36 +795,38 @@ fn spawn_popup(menu: Query<Entity, With<DemoMenuButton>>, mut commands: Commands
|
|||||||
let Ok(anchor) = menu.single() else {
|
let Ok(anchor) = menu.single() else {
|
||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
commands.entity(anchor).insert(PortalChildren::spawn_one((
|
let menu = commands
|
||||||
Node {
|
.spawn((
|
||||||
min_height: Val::Px(100.),
|
Node {
|
||||||
min_width: Val::Px(100.),
|
min_height: Val::Px(100.),
|
||||||
border: UiRect::all(Val::Px(2.0)),
|
min_width: Val::Percent(100.),
|
||||||
position_type: PositionType::Absolute,
|
border: UiRect::all(Val::Px(2.0)),
|
||||||
left: Val::Px(100.),
|
position_type: PositionType::Absolute,
|
||||||
..default()
|
..default()
|
||||||
},
|
},
|
||||||
BorderColor::all(GREEN.into()),
|
CoreMenuPopup,
|
||||||
BackgroundColor(GRAY.into()),
|
Visibility::Hidden, // Will be visible after positioning
|
||||||
ZIndex(100),
|
BorderColor::all(GREEN.into()),
|
||||||
Floating {
|
BackgroundColor(GRAY.into()),
|
||||||
anchor: FloatAnchor::Node(anchor),
|
ZIndex(100),
|
||||||
positions: vec![
|
Popover {
|
||||||
FloatPosition {
|
positions: vec![
|
||||||
side: FloatSide::Bottom,
|
PopoverPlacement {
|
||||||
align: FloatAlign::Start,
|
side: PopoverSide::Bottom,
|
||||||
gap: 2.0,
|
align: PopoverAlign::Start,
|
||||||
..default()
|
gap: 2.0,
|
||||||
},
|
},
|
||||||
FloatPosition {
|
PopoverPlacement {
|
||||||
side: FloatSide::Top,
|
side: PopoverSide::Top,
|
||||||
align: FloatAlign::Start,
|
align: PopoverAlign::Start,
|
||||||
gap: 2.0,
|
gap: 2.0,
|
||||||
..default()
|
},
|
||||||
},
|
],
|
||||||
],
|
},
|
||||||
},
|
OverrideClip,
|
||||||
)));
|
))
|
||||||
|
.id();
|
||||||
|
commands.entity(anchor).add_child(menu);
|
||||||
info!("Open menu");
|
info!("Open menu");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user