Merge branch 'FyroxEngine:master' into inspector-fix

This commit is contained in:
b-guild
2024-04-13 19:17:39 -07:00
committed by GitHub
4 changed files with 336 additions and 65 deletions
+310 -57
View File
@@ -7,13 +7,14 @@ use crate::{
border::BorderBuilder,
brush::Brush,
core::{
algebra::Vector2, color::Color, parking_lot::Mutex, pool::Handle, reflect::prelude::*,
type_traits::prelude::*, uuid_provider, visitor::prelude::*,
algebra::Vector2, color::Color, pool::Handle, reflect::prelude::*, type_traits::prelude::*,
uuid_provider, variable::InheritableVariable, visitor::prelude::*,
},
decorator::{DecoratorBuilder, DecoratorMessage},
define_constructor,
draw::DrawingContext,
grid::{Column, GridBuilder, Row},
message::{ButtonState, MessageDirection, OsEvent, UiMessage},
message::{ButtonState, KeyCode, MessageDirection, OsEvent, UiMessage},
popup::{Placement, Popup, PopupBuilder, PopupMessage},
stack_panel::StackPanelBuilder,
text::TextBuilder,
@@ -23,12 +24,11 @@ use crate::{
BuildContext, Control, HorizontalAlignment, Orientation, RestrictionEntry, Thickness, UiNode,
UserInterface, VerticalAlignment, BRUSH_BRIGHT, BRUSH_BRIGHT_BLUE, BRUSH_PRIMARY,
};
use fyrox_core::variable::InheritableVariable;
use fyrox_graph::{BaseSceneGraph, SceneGraph};
use fyrox_graph::{BaseSceneGraph, SceneGraph, SceneGraphNode};
use std::any::TypeId;
use std::{
ops::{Deref, DerefMut},
sync::mpsc::Sender,
sync::Arc,
};
/// A set of messages that can be used to manipulate a [`Menu`] widget at runtime.
@@ -58,7 +58,10 @@ pub enum MenuItemMessage {
/// Opens the menu item's popup with inner items.
Open,
/// Closes the menu item's popup with inner items.
Close,
Close {
/// Defines, whether the item should be deselected when closed or not.
deselect: bool,
},
/// The message is generated by a menu item when it is clicked.
Click,
/// Adds a new item to the menu item.
@@ -67,6 +70,8 @@ pub enum MenuItemMessage {
RemoveItem(Handle<UiNode>),
/// Sets the new items of the menu item.
Items(Vec<Handle<UiNode>>),
/// Selects/deselects the item.
Select(bool),
}
impl MenuItemMessage {
@@ -76,7 +81,7 @@ impl MenuItemMessage {
);
define_constructor!(
/// Creates [`MenuItemMessage::Close`] message.
MenuItemMessage:Close => fn close(), layout: false
MenuItemMessage:Close => fn close(deselect: bool), layout: false
);
define_constructor!(
/// Creates [`MenuItemMessage::Click`] message.
@@ -94,6 +99,10 @@ impl MenuItemMessage {
/// Creates [`MenuItemMessage::Items`] message.
MenuItemMessage:Items => fn items(Vec<Handle<UiNode>>), layout: false
);
define_constructor!(
/// Creates [`MenuItemMessage::Select`] message.
MenuItemMessage:Select => fn select(bool), layout: false
);
}
/// Menu widget is a root widget of an arbitrary menu hierarchy. An example could be "standard" menu strip with `File`, `Edit`, `View`, etc.
@@ -151,6 +160,8 @@ impl MenuItemMessage {
pub struct Menu {
widget: Widget,
active: bool,
#[component(include)]
items: ItemsContainer,
}
crate::define_widget_deref!(Menu);
@@ -185,10 +196,11 @@ impl Control for Menu {
ui.send_message(MenuItemMessage::close(
handle,
MessageDirection::ToWidget,
true,
));
// We have to search in popup content too because menu shows its content
// in popup and content could be another menu item.
stack.push(*item.popup);
stack.push(*item.items_panel);
}
// Continue depth search.
stack.extend_from_slice(node.children());
@@ -196,6 +208,11 @@ impl Control for Menu {
}
}
}
} else if let Some(WidgetMessage::KeyDown(key_code)) = message.data() {
if !message.handled() {
message.set_handled(true);
keyboard_navigation(ui, *key_code, self, self.handle);
}
}
}
@@ -221,7 +238,7 @@ impl Control for Menu {
'depth_search: while let Some(handle) = stack.pop() {
let node = ui.node(handle);
if let Some(item) = node.cast::<MenuItem>() {
let popup = ui.node(*item.popup);
let popup = ui.node(*item.items_panel);
if popup.screen_bounds().contains(pos) && popup.is_globally_visible() {
// Once we found that we clicked inside some descendant menu item
// we can immediately stop search - we don't want to close menu
@@ -231,7 +248,7 @@ impl Control for Menu {
}
// We have to search in popup content too because menu shows its content
// in popup and content could be another menu item.
stack.push(*item.popup);
stack.push(*item.items_panel);
}
// Continue depth search.
stack.extend_from_slice(node.children());
@@ -259,6 +276,41 @@ pub enum MenuItemPlacement {
Right,
}
#[derive(Default, Clone, Debug, Visit, Reflect, ComponentProvider)]
#[doc(hidden)]
pub struct ItemsContainer {
#[doc(hidden)]
pub items: InheritableVariable<Vec<Handle<UiNode>>>,
}
impl Deref for ItemsContainer {
type Target = Vec<Handle<UiNode>>;
fn deref(&self) -> &Self::Target {
self.items.deref()
}
}
impl DerefMut for ItemsContainer {
fn deref_mut(&mut self) -> &mut Self::Target {
self.items.deref_mut()
}
}
impl ItemsContainer {
fn selected_item_index(&self, ui: &UserInterface) -> Option<usize> {
for (index, item) in self.items.iter().enumerate() {
if let Some(item_ref) = ui.try_get_of_type::<MenuItem>(*item) {
if *item_ref.is_selected {
return Some(index);
}
}
}
None
}
}
/// Menu item is a widget with arbitrary content, that has a "floating" panel (popup) for sub-items if the menu item. This was menu items can form
/// arbitrary hierarchies. See [`Menu`] docs for examples.
#[derive(Default, Clone, Debug, Visit, Reflect, ComponentProvider)]
@@ -266,9 +318,10 @@ pub struct MenuItem {
/// Base widget of the menu item.
pub widget: Widget,
/// Current items of the menu item
pub items: InheritableVariable<Vec<Handle<UiNode>>>,
#[component(include)]
pub items_container: ItemsContainer,
/// A handle of a popup that holds the items of the menu item.
pub popup: InheritableVariable<Handle<UiNode>>,
pub items_panel: InheritableVariable<Handle<UiNode>>,
/// A handle of a panel widget that arranges items of the menu item.
pub panel: InheritableVariable<Handle<UiNode>>,
/// Current placement of the menu item.
@@ -277,6 +330,8 @@ pub struct MenuItem {
pub clickable_when_not_empty: InheritableVariable<bool>,
/// A handle to the decorator of the item.
pub decorator: InheritableVariable<Handle<UiNode>>,
/// Is this item selected or not.
pub is_selected: InheritableVariable<bool>,
}
crate::define_widget_deref!(MenuItem);
@@ -289,11 +344,9 @@ crate::define_widget_deref!(MenuItem);
fn find_menu(from: Handle<UiNode>, ui: &UserInterface) -> Handle<UiNode> {
let mut handle = from;
while handle.is_some() {
if let Some((_, popup)) = ui.find_component_up::<Popup>(handle) {
if let Some((_, panel)) = ui.find_component_up::<MenuItemsPanel>(handle) {
// Continue search from parent menu item of popup.
handle = popup
.user_data_cloned::<Handle<UiNode>>()
.unwrap_or_default();
handle = panel.parent_menu_item;
} else {
// Maybe we have Menu as parent for MenuItem.
return ui.find_handle_up(handle, &mut |n| n.cast::<Menu>().is_some());
@@ -321,18 +374,16 @@ fn is_any_menu_item_contains_point(ui: &UserInterface, pt: Vector2<f32>) -> bool
fn close_menu_chain(from: Handle<UiNode>, ui: &UserInterface) {
let mut handle = from;
while handle.is_some() {
let popup_handle = ui.find_handle_up(handle, &mut |n| n.has_component::<Popup>());
let popup_handle = ui.find_handle_up(handle, &mut |n| n.has_component::<MenuItemsPanel>());
if let Some(popup) = ui.node(popup_handle).query_component::<Popup>() {
if let Some(panel) = ui.node(popup_handle).query_component::<MenuItemsPanel>() {
ui.send_message(PopupMessage::close(
popup_handle,
MessageDirection::ToWidget,
));
// Continue search from parent menu item of popup.
handle = popup
.user_data_cloned::<Handle<UiNode>>()
.unwrap_or_default();
handle = panel.parent_menu_item;
} else {
// Prevent infinite loops.
break;
@@ -348,7 +399,7 @@ impl Control for MenuItem {
// So we have to remove it manually.
sender
.send(WidgetMessage::remove(
*self.popup,
*self.items_panel,
MessageDirection::ToWidget,
))
.unwrap();
@@ -374,13 +425,13 @@ impl Control for MenuItem {
}
WidgetMessage::MouseUp { .. } => {
if !message.handled() {
if self.items.is_empty() || *self.clickable_when_not_empty {
if self.items_container.is_empty() || *self.clickable_when_not_empty {
ui.send_message(MenuItemMessage::click(
self.handle(),
MessageDirection::ToWidget,
));
}
if self.items.is_empty() {
if self.items_container.is_empty() {
let menu = find_menu(self.parent(), ui);
if menu.is_some() {
// Deactivate menu if we have one.
@@ -416,6 +467,17 @@ impl Control for MenuItem {
));
}
}
WidgetMessage::KeyDown(key_code) => {
if !message.handled() && *self.is_selected && *key_code == KeyCode::Enter {
ui.send_message(MenuItemMessage::click(
self.handle,
MessageDirection::FromWidget,
));
let menu = find_menu(self.parent(), ui);
ui.send_message(MenuMessage::deactivate(menu, MessageDirection::ToWidget));
message.set_handled(true);
}
}
_ => {}
}
} else if let Some(msg) = message.data::<MenuItemMessage>() {
@@ -423,40 +485,60 @@ impl Control for MenuItem {
&& message.direction() == MessageDirection::ToWidget
{
match msg {
MenuItemMessage::Select(selected) => {
if *self.is_selected != *selected {
self.is_selected.set_value_and_mark_modified(*selected);
ui.send_message(DecoratorMessage::select(
*self.decorator,
MessageDirection::ToWidget,
*selected,
));
ui.send_message(WidgetMessage::focus(
self.handle,
MessageDirection::ToWidget,
));
}
}
MenuItemMessage::Open => {
if !self.items.is_empty() {
if !self.items_container.is_empty() {
let placement = match *self.placement {
MenuItemPlacement::Bottom => Placement::LeftBottom(self.handle),
MenuItemPlacement::Right => Placement::RightTop(self.handle),
};
if !*self.is_selected {
ui.send_message(MenuItemMessage::select(
self.handle,
MessageDirection::ToWidget,
true,
));
}
// Open popup.
ui.send_message(PopupMessage::placement(
*self.popup,
*self.items_panel,
MessageDirection::ToWidget,
placement,
));
ui.send_message(PopupMessage::open(
*self.popup,
*self.items_panel,
MessageDirection::ToWidget,
));
ui.send_message(DecoratorMessage::select(
*self.decorator,
MessageDirection::ToWidget,
true,
));
}
}
MenuItemMessage::Close => {
MenuItemMessage::Close { deselect } => {
ui.send_message(PopupMessage::close(
*self.popup,
*self.items_panel,
MessageDirection::ToWidget,
));
ui.send_message(DecoratorMessage::select(
*self.decorator,
MessageDirection::ToWidget,
false,
));
if *deselect && *self.is_selected {
ui.send_message(DecoratorMessage::select(
*self.decorator,
MessageDirection::ToWidget,
false,
));
}
}
MenuItemMessage::Click => {}
MenuItemMessage::AddItem(item) => {
@@ -465,11 +547,13 @@ impl Control for MenuItem {
MessageDirection::ToWidget,
*self.panel,
));
self.items.push(*item);
self.items_container.push(*item);
}
MenuItemMessage::RemoveItem(item) => {
if let Some(position) = self.items.iter().position(|i| *i == *item) {
self.items.remove(position);
if let Some(position) =
self.items_container.iter().position(|i| *i == *item)
{
self.items_container.remove(position);
ui.send_message(WidgetMessage::remove(
*item,
@@ -478,7 +562,7 @@ impl Control for MenuItem {
}
}
MenuItemMessage::Items(items) => {
for &current_item in self.items.iter() {
for &current_item in self.items_container.iter() {
ui.send_message(WidgetMessage::remove(
current_item,
MessageDirection::ToWidget,
@@ -493,7 +577,9 @@ impl Control for MenuItem {
));
}
self.items.set_value_and_mark_modified(items.clone());
self.items_container
.items
.set_value_and_mark_modified(items.clone());
}
}
}
@@ -513,12 +599,10 @@ impl Control for MenuItem {
break;
} else {
let node = ui.node(handle);
if let Some(popup) = node.cast::<Popup>() {
if let Some(panel) = node.component_ref::<MenuItemsPanel>() {
// Once we found popup in chain, we must extract handle
// of parent menu item to continue search.
handle = popup
.user_data_cloned::<Handle<UiNode>>()
.unwrap_or_default();
handle = panel.parent_menu_item;
} else {
handle = node.parent();
}
@@ -529,6 +613,7 @@ impl Control for MenuItem {
ui.send_message(MenuItemMessage::close(
self.handle(),
MessageDirection::ToWidget,
true,
));
}
}
@@ -544,14 +629,17 @@ impl Control for MenuItem {
// Allow closing "orphaned" menus by clicking outside of them.
if let OsEvent::MouseInput { state, .. } = event {
if *state == ButtonState::Pressed {
if let Some(popup) = ui.node(*self.popup).query_component::<Popup>() {
if *popup.is_open {
if let Some(panel) = ui
.node(*self.items_panel)
.query_component::<MenuItemsPanel>()
{
if *panel.popup.is_open {
// Ensure that cursor is outside of any menus.
if !is_any_menu_item_contains_point(ui, ui.cursor_position())
&& find_menu(self.parent(), ui).is_none()
{
ui.send_message(PopupMessage::close(
*self.popup,
*self.items_panel,
MessageDirection::ToWidget,
));
@@ -615,6 +703,9 @@ impl MenuBuilder {
.with_child(back)
.build(),
active: false,
items: ItemsContainer {
items: self.items.into(),
},
};
ctx.add_node(UiNode::new(menu))
@@ -835,7 +926,11 @@ impl<'a, 'b> MenuItemBuilder<'a, 'b> {
})
// We'll manually control if popup is either open or closed.
.stays_open(true)
.build(ctx);
.build_popup(ctx);
let items_panel = ctx.add_node(UiNode::new(MenuItemsPanel {
popup,
parent_menu_item: Default::default(),
}));
let menu = MenuItem {
widget: self
@@ -844,21 +939,179 @@ impl<'a, 'b> MenuItemBuilder<'a, 'b> {
.with_preview_messages(true)
.with_child(decorator)
.build(),
popup: popup.into(),
items: self.items.into(),
items_panel: items_panel.into(),
items_container: ItemsContainer {
items: self.items.into(),
},
placement: MenuItemPlacement::Right.into(),
panel: panel.into(),
clickable_when_not_empty: false.into(),
decorator: decorator.into(),
is_selected: Default::default(),
};
let handle = ctx.add_node(UiNode::new(menu));
// "Link" popup with its parent menu item.
if let Some(popup) = ctx[popup].cast_mut::<Popup>() {
popup.user_data = Some(Arc::new(Mutex::new(handle)));
if let Some(popup) = ctx[items_panel].cast_mut::<MenuItemsPanel>() {
popup.parent_menu_item = handle;
}
handle
}
}
/// A simple wrapper over [`Popup`] widget, that holds the sub-items of a menu item and provides
/// an ability for keyboard navigation.
#[derive(Default, Clone, Debug, Visit, Reflect, TypeUuidProvider, ComponentProvider)]
#[type_uuid(id = "ad8e9e76-c213-4232-9bab-80ebcabd69fa")]
pub struct MenuItemsPanel {
#[component(include)]
popup: Popup,
parent_menu_item: Handle<UiNode>,
}
impl Deref for MenuItemsPanel {
type Target = Widget;
fn deref(&self) -> &Self::Target {
&self.popup.widget
}
}
impl DerefMut for MenuItemsPanel {
fn deref_mut(&mut self) -> &mut Self::Target {
&mut self.popup.widget
}
}
impl Control for MenuItemsPanel {
fn on_remove(&self, sender: &Sender<UiMessage>) {
self.popup.on_remove(sender)
}
fn measure_override(&self, ui: &UserInterface, available_size: Vector2<f32>) -> Vector2<f32> {
self.popup.measure_override(ui, available_size)
}
fn arrange_override(&self, ui: &UserInterface, final_size: Vector2<f32>) -> Vector2<f32> {
self.popup.arrange_override(ui, final_size)
}
fn draw(&self, drawing_context: &mut DrawingContext) {
self.popup.draw(drawing_context)
}
fn post_draw(&self, drawing_context: &mut DrawingContext) {
self.popup.post_draw(drawing_context)
}
fn update(&mut self, dt: f32, ui: &mut UserInterface) {
self.popup.update(dt, ui);
}
fn handle_routed_message(&mut self, ui: &mut UserInterface, message: &mut UiMessage) {
self.popup.handle_routed_message(ui, message);
if let Some(WidgetMessage::KeyDown(key_code)) = message.data() {
if !message.handled() {
message.set_handled(true);
if let Some(parent_menu_item) = ui.try_get(self.parent_menu_item) {
keyboard_navigation(
ui,
*key_code,
parent_menu_item.deref(),
self.parent_menu_item,
);
}
}
}
}
fn preview_message(&self, ui: &UserInterface, message: &mut UiMessage) {
self.popup.preview_message(ui, message)
}
fn handle_os_event(
&mut self,
self_handle: Handle<UiNode>,
ui: &mut UserInterface,
event: &OsEvent,
) {
self.popup.handle_os_event(self_handle, ui, event)
}
}
fn keyboard_navigation(
ui: &UserInterface,
key_code: KeyCode,
parent_menu_item: &dyn Control,
parent_menu_item_handle: Handle<UiNode>,
) {
let Some(items_container) = parent_menu_item
.query_component_ref(TypeId::of::<ItemsContainer>())
.and_then(|c| c.downcast_ref::<ItemsContainer>())
else {
return;
};
match key_code {
KeyCode::ArrowLeft => {
ui.send_message(MenuItemMessage::close(
parent_menu_item_handle,
MessageDirection::ToWidget,
false,
));
}
KeyCode::ArrowRight => {
if let Some(selected_item_index) = items_container.selected_item_index(ui) {
let selected_item = items_container.items[selected_item_index];
ui.send_message(MenuItemMessage::open(
selected_item,
MessageDirection::ToWidget,
));
if let Some(selected_item_ref) = ui.try_get_of_type::<MenuItem>(selected_item) {
if let Some(first_item) = selected_item_ref.items_container.first() {
ui.send_message(MenuItemMessage::select(
*first_item,
MessageDirection::ToWidget,
true,
));
}
}
}
}
KeyCode::ArrowUp | KeyCode::ArrowDown => {
if let Some(selected_item_index) = items_container.selected_item_index(ui) {
let new_selection_index = match key_code {
KeyCode::ArrowUp => selected_item_index.saturating_sub(1),
KeyCode::ArrowDown => selected_item_index.saturating_add(1),
_ => unreachable!(),
};
if let Some(new_selection) = items_container.items.get(new_selection_index) {
ui.send_message(MenuItemMessage::select(
items_container.items[selected_item_index],
MessageDirection::ToWidget,
false,
));
ui.send_message(MenuItemMessage::select(
*new_selection,
MessageDirection::ToWidget,
true,
));
}
} else if let Some(first_item) = items_container.items.first() {
ui.send_message(MenuItemMessage::select(
*first_item,
MessageDirection::ToWidget,
true,
));
}
}
_ => (),
}
}
+5 -1
View File
@@ -21,11 +21,12 @@ use crate::{
inspector::Inspector,
key::{HotKeyEditor, KeyBindingEditor},
list_view::{ListView, ListViewItem},
menu::{Menu, MenuItem},
menu::{Menu, MenuItem, MenuItemsPanel},
messagebox::MessageBox,
nine_patch::NinePatch,
numeric::NumericUpDown,
path::PathEditor,
popup::Popup,
progress_bar::ProgressBar,
range::RangeEditor,
rect::RectEditor,
@@ -94,6 +95,7 @@ impl WidgetConstructorContainer {
container.add::<ListView>();
container.add::<Menu>();
container.add::<MenuItem>();
container.add::<MenuItemsPanel>();
container.add::<MessageBox>();
container.add::<NinePatch>();
@@ -165,6 +167,8 @@ impl WidgetConstructorContainer {
container.add::<Inspector>();
container.add::<Popup>();
container.add::<VecEditor<u8, 2>>();
container.add::<VecEditor<i8, 2>>();
container.add::<VecEditor<u16, 2>>();
+7 -1
View File
@@ -98,6 +98,12 @@ pub enum Placement {
},
}
impl Default for Placement {
fn default() -> Self {
Self::LeftTop(Default::default())
}
}
impl Placement {
/// Returns a handle of the node to which this placement corresponds to.
pub fn target(&self) -> Handle<UiNode> {
@@ -265,7 +271,7 @@ impl Placement {
///
/// Popup widget can automatically adjust its position to always remain on screen, which is useful for tooltips, dropdown lists,
/// etc. To enable this option, use [`PopupBuilder::with_smart_placement`] with `true` as the first argument.
#[derive(Clone, Visit, Debug, Reflect, ComponentProvider)]
#[derive(Default, Clone, Visit, Debug, Reflect, ComponentProvider)]
pub struct Popup {
/// Base widget of the popup.
pub widget: Widget,
+14 -6
View File
@@ -249,14 +249,22 @@ impl StackPanelBuilder {
self
}
/// Finishes stack panel building.
pub fn build_stack_panel(self) -> StackPanel {
StackPanel {
widget: self.widget_builder.build(),
orientation: self.orientation.unwrap_or(Orientation::Vertical).into(),
}
}
/// Finishes stack panel building and wraps the result in a UI node.
pub fn build_node(self) -> UiNode {
UiNode::new(self.build_stack_panel())
}
/// Finishes stack panel building and adds the new stack panel widget instance to the user interface and
/// returns its handle.
pub fn build(self, ctx: &mut BuildContext) -> Handle<UiNode> {
let stack_panel = StackPanel {
widget: self.widget_builder.build(),
orientation: self.orientation.unwrap_or(Orientation::Vertical).into(),
};
ctx.add_node(UiNode::new(stack_panel))
ctx.add_node(self.build_node())
}
}