diff --git a/fyrox-ui/src/menu.rs b/fyrox-ui/src/menu.rs index 43dcd7192..3eb5e009f 100644 --- a/fyrox-ui/src/menu.rs +++ b/fyrox-ui/src/menu.rs @@ -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), /// Sets the new items of the menu item. Items(Vec>), + /// 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>), 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::() { - 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>>, +} + +impl Deref for ItemsContainer { + type Target = Vec>; + + 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 { + for (index, item) in self.items.iter().enumerate() { + if let Some(item_ref) = ui.try_get_of_type::(*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>>, + #[component(include)] + pub items_container: ItemsContainer, /// A handle of a popup that holds the items of the menu item. - pub popup: InheritableVariable>, + pub items_panel: InheritableVariable>, /// A handle of a panel widget that arranges items of the menu item. pub panel: InheritableVariable>, /// Current placement of the menu item. @@ -277,6 +330,8 @@ pub struct MenuItem { pub clickable_when_not_empty: InheritableVariable, /// A handle to the decorator of the item. pub decorator: InheritableVariable>, + /// Is this item selected or not. + pub is_selected: InheritableVariable, } crate::define_widget_deref!(MenuItem); @@ -289,11 +344,9 @@ crate::define_widget_deref!(MenuItem); fn find_menu(from: Handle, ui: &UserInterface) -> Handle { let mut handle = from; while handle.is_some() { - if let Some((_, popup)) = ui.find_component_up::(handle) { + if let Some((_, panel)) = ui.find_component_up::(handle) { // Continue search from parent menu item of popup. - handle = popup - .user_data_cloned::>() - .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::().is_some()); @@ -321,18 +374,16 @@ fn is_any_menu_item_contains_point(ui: &UserInterface, pt: Vector2) -> bool fn close_menu_chain(from: Handle, ui: &UserInterface) { let mut handle = from; while handle.is_some() { - let popup_handle = ui.find_handle_up(handle, &mut |n| n.has_component::()); + let popup_handle = ui.find_handle_up(handle, &mut |n| n.has_component::()); - if let Some(popup) = ui.node(popup_handle).query_component::() { + if let Some(panel) = ui.node(popup_handle).query_component::() { ui.send_message(PopupMessage::close( popup_handle, MessageDirection::ToWidget, )); // Continue search from parent menu item of popup. - handle = popup - .user_data_cloned::>() - .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::() { @@ -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 ¤t_item in self.items.iter() { + for ¤t_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::() { + if let Some(panel) = node.component_ref::() { // Once we found popup in chain, we must extract handle // of parent menu item to continue search. - handle = popup - .user_data_cloned::>() - .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::() { - if *popup.is_open { + if let Some(panel) = ui + .node(*self.items_panel) + .query_component::() + { + 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.user_data = Some(Arc::new(Mutex::new(handle))); + if let Some(popup) = ctx[items_panel].cast_mut::() { + 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, +} + +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) { + self.popup.on_remove(sender) + } + + fn measure_override(&self, ui: &UserInterface, available_size: Vector2) -> Vector2 { + self.popup.measure_override(ui, available_size) + } + + fn arrange_override(&self, ui: &UserInterface, final_size: Vector2) -> Vector2 { + 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, + 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, +) { + let Some(items_container) = parent_menu_item + .query_component_ref(TypeId::of::()) + .and_then(|c| c.downcast_ref::()) + 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::(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, + )); + } + } + + _ => (), + } +} diff --git a/fyrox-ui/src/node/constructor.rs b/fyrox-ui/src/node/constructor.rs index acb77cda4..09631dec9 100644 --- a/fyrox-ui/src/node/constructor.rs +++ b/fyrox-ui/src/node/constructor.rs @@ -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::(); container.add::(); container.add::(); + container.add::(); container.add::(); container.add::(); @@ -165,6 +167,8 @@ impl WidgetConstructorContainer { container.add::(); + container.add::(); + container.add::>(); container.add::>(); container.add::>(); diff --git a/fyrox-ui/src/popup.rs b/fyrox-ui/src/popup.rs index 359704779..a951ed85d 100644 --- a/fyrox-ui/src/popup.rs +++ b/fyrox-ui/src/popup.rs @@ -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 { @@ -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, diff --git a/fyrox-ui/src/stack_panel.rs b/fyrox-ui/src/stack_panel.rs index 797713953..203021e17 100644 --- a/fyrox-ui/src/stack_panel.rs +++ b/fyrox-ui/src/stack_panel.rs @@ -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 { - 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()) } }